C++ Random Number Engines and Distributions Guide
Modern C++ provides a robust, mathematically sound framework for
generating random values through the <random>
library, introduced in C++11. This article explains the two-part
architecture of standard random number generation in C++: pseudo-random
number engines like std::mt19937 that produce sequences of
raw bits, and probability distributions that transform those bits into
meaningful ranges and patterns. You will learn how engines work, how
distributions shape data, and how to combine them correctly in
production code.
The Two-Part Architecture
of <random>
In legacy C and older C++, random numbers were generated using
rand(), which combined generation and range scaling into an
inflexible, low-entropy mechanism with noticeable statistical bias.
Modern C++ decouples this task into two distinct components:
- Engines: State-driven state machines that generate a sequence of uniformly distributed, pseudo-random unsigned integers.
- Distributions: Post-processors that accept raw integers from an engine and map them into specific ranges, types (integers or floating-point values), and statistical profiles (uniform, normal, Poisson, etc.).
This separation ensures statistical fairness, portability across platforms, and flexibility across diverse simulation and gaming domains.
Standard Random Number Engines
A random number engine maintains an internal state and advances it each time an output is requested. C++ provides several predefined engine types:
- Mersenne Twister (
std::mt19937andstd::mt19937_64): The most widely used standard engine.std::mt19937produces 32-bit integers, whilestd::mt19937_64produces 64-bit integers. It is based on the Mersenne Twister algorithm developed by Makoto Matsumoto and Takuji Nishimura. It features an exceptionally long period of \(2^{19937}-1\) and passes most rigorous statistical randomness tests. It is fast and suitable for Monte Carlo simulations, games, and procedural generation, though it is not cryptographically secure. - Linear Congruential (
std::minstd_rand,std::minstd_rand0): Extremely lightweight engines with small memory footprints. They use simple linear recurrence relationships. While fast, they have shorter periods and exhibit dimensional artifacts, making them unsuitable for precision simulations. - Subtract-with-Carry (
std::ranlux24,std::ranlux48): Slower engines designed to discard intermediate states to eliminate correlations, used primarily in high-precision scientific computing. std::default_random_engine: An implementation-defined alias chosen by the compiler vendor. Because its underlying algorithm changes across platforms and compiler versions, explicit engines likestd::mt19937are preferred for reproducible behavior.
Seeding Engines with
std::random_device
Engines are deterministic; using the same initial seed will always
generate the exact same sequence of values. To produce different
sequences on each program run, engines are typically seeded using
std::random_device.
std::random_device attempts to source non-deterministic
entropy from the operating system or hardware (such as CPU thermal
noise). Because reading from hardware entropy can be relatively slow,
std::random_device should only be used to seed an engine
once, rather than generating values directly.
Standard Distributions
Distributions take the uniform integer output of an engine and shape it mathematically to match a specified statistical model without introducing modulo bias.
- Uniform Distributions:
std::uniform_int_distribution<T>: Produces integers within a closed interval[a, b]where every integer has an equal probability of occurrence.std::uniform_real_distribution<T>: Produces floating-point numbers within a half-open interval[a, b).
- Bernoulli and Sampling Distributions:
std::bernoulli_distribution: Returns a boolean with a specified probabilitypof returningtrue.std::binomial_distribution: Models the number of successes in a sequence of independent trials.
- Normal and Poisson Distributions:
std::normal_distribution<T>: Generates floating-point values clustered around a mean (\(\mu\)) with a specific standard deviation (\(\sigma\)), forming a Gaussian bell curve.std::poisson_distribution<T>: Models the frequency of independent events occurring within a fixed interval.
Practical Implementation
The following example demonstrates how to seed an engine, generate raw values, and transform them using uniform integer and normal distributions:
#include <iostream>
#include <random>
int main() {
// 1. Obtain a hardware entropy source for seeding
std::random_device rd;
// 2. Initialize the standard 64-bit Mersenne Twister engine
std::mt19937_64 engine(rd());
// 3. Define a uniform distribution between 1 and 6 (e.g., a die roll)
std::uniform_int_distribution<int> dice_dist(1, 6);
// 4. Define a normal distribution with mean 170.0 and std deviation 10.0
std::normal_distribution<double> height_dist(170.0, 10.0);
// Generate samples by passing the engine to the distribution
std::cout << "Uniform Die Roll: " << dice_dist(engine) << '\n';
std::cout << "Sampled Height: " << height_dist(engine) << '\n';
return 0;
}Best Practices
- Instantiate engines sparingly: Re-initializing an engine inside a loop or function can re-seed the generator or cause it to repeat sequences. Keep engines long-lived (e.g., as member variables, thread-local variables, or by passing them by reference).
- Thread safety: Standard random engines are not
thread-safe. Multiple threads accessing the same engine instance must
synchronize access or, preferably, use their own
thread_local std::mt19937instances. - Cryptography requirements: Neither
std::mt19937nor any other standard C++ engine is cryptographically secure. Their internal states can be determined after observing a small number of consecutive outputs. For security-sensitive applications (such as password generation, session tokens, or key derivation), platform-specific cryptographic APIs (like OpenSSL, Windows CNG, orgetrandom) must be used instead.