Random Number Generation
C++ <random> engines, distributions, seeding strategies, thread safety, and common pitfalls — from mt19937 to discrete_distribution.
<random>since C++11The <random> header separates concerns into engines (stateful bit sources) and distributions (mappings from engine output to a statistical distribution), eliminating the modulo bias and weak state of the legacy rand() API.
Overview
The design is composable: any engine works with any distribution. The two most important choices are:
- Engine:
std::mt19937(32-bit) orstd::mt19937_64(64-bit) for virtually all non-cryptographic uses. - Seeding:
std::random_devicefor entropy, optionally fed throughstd::seed_seqto saturate the full engine state.
std::random_device produces non-deterministic output when a hardware entropy source is available. On some implementations (notably MinGW on Windows before fixes), it returns a constant — check rd.entropy() > 0 as a hint, though the standard does not require implementations to return non-zero even when the source is truly random.
Engines
#include <random>
// Mersenne Twister, 32-bit output — fast, 624-word state, period 2^19937-1 (C++11)
std::mt19937 rng32;
// 64-bit variant — prefer when generating 64-bit values or large ranges (C++11)
std::mt19937_64 rng64;
// Linear congruential — tiny state, fast, but visibly correlated (C++11)
// Avoid for simulations; fine for throwaway noise.
std::minstd_rand fast_rng;
// Non-deterministic source — use only for seeding, not in tight loops (C++11)
std::random_device rd;
unsigned int entropy = rd();
// default_random_engine — implementation-defined; differs across compilers/OSes.
// Never use in portable or reproducible code.
std::default_random_engine avoid_this;Seeding
Seeding mt19937 with a single 32-bit value leaves 19,905 bits of state at their default, producing correlated initial output for different seeds that differ only slightly. For quality work, fill the full state via std::seed_seq:
// Minimal seeding — fine for most applications (C++11)
std::mt19937 rng(std::random_device{}());
// Full-state seeding — eliminates seed correlation issues (C++11)
std::seed_seq seq{
std::random_device{}(), std::random_device{}(),
std::random_device{}(), std::random_device{}(),
std::random_device{}(), std::random_device{}(),
std::random_device{}(), std::random_device{}()
};
std::mt19937 rng_full(seq);
// Reproducible — for tests, benchmarks, replays
std::mt19937 test_rng(42);seed_seq applies a mixing function, so you do not need 624 calls to random_device — 8 words saturate entropy adequately and amortize the kernel-call overhead.
Distributions
Uniform Integer
std::uniform_int_distribution<int> d6(1, 6); // [1, 6] inclusive
std::uniform_int_distribution<int> coin(0, 1);
std::uniform_int_distribution<std::ptrdiff_t> idx(0, static_cast<std::ptrdiff_t>(v.size()) - 1);The distribution object is reusable and stateless between calls — construct it once per range, not per draw.
Uniform Real
// [0.0, 1.0) — half-open on the right (C++11)
std::uniform_real_distribution<double> unit(0.0, 1.0);
// Full-circle angle
std::uniform_real_distribution<float> angle(0.0f, 2.0f * std::numbers::pi_v<float>); // std::numbers: C++20Normal (Gaussian)
std::normal_distribution<double> standard(0.0, 1.0); // mean, stddev
std::normal_distribution<double> heights(175.0, 10.0); // human height in cm
double z = standard(rng);Internally uses the Box-Muller or ziggurat algorithm depending on the implementation. The distribution caches one value from each pair, so consecutive calls are not independent draws in all implementations — this is standard-conforming behaviour.
Bernoulli
std::bernoulli_distribution fair(0.5);
std::bernoulli_distribution biased(0.7); // true 70% of the time
bool outcome = fair(rng);Discrete (Weighted Choice)
// Probability proportional to weight: P(i) = w[i] / sum(w)
std::discrete_distribution<int> weighted({1.0, 4.0, 2.0});
// P(0)=1/7, P(1)=4/7, P(2)=2/7
int idx = weighted(rng);Weights need not sum to 1 — the distribution normalises them internally. Negative weights are undefined behaviour.
Poisson Family
std::poisson_distribution<int> arrivals(3.5); // mean events per interval
std::exponential_distribution<double> inter(2.0); // rate λ=2, mean=0.5s
std::geometric_distribution<int> geom(0.3); // trials until first success
int n = arrivals(rng);
double t = inter(rng);Full Distribution Roster
std::binomial_distribution<int> binom(10, 0.5);
std::negative_binomial_distribution<int> nbinom(3, 0.4);
std::gamma_distribution<double> gam(2.0, 1.0); // shape, scale
std::chi_squared_distribution<double> chi2(5.0);
std::student_t_distribution<double> t_dist(10.0);
std::lognormal_distribution<double> lognorm(0.0, 1.0);
std::cauchy_distribution<double> cauchy(0.0, 1.0);
std::weibull_distribution<double> weibull(1.5, 2.0);
std::extreme_value_distribution<double> gumbel(0.0, 1.0);
std::piecewise_linear_distribution<double> pld({0,1,2,3}, {0,1,0.5,0});Examples
Shuffle and Sample
#include <random>
#include <numeric>
#include <algorithm>
#include <vector>
std::mt19937 rng(std::random_device{}());
// Fisher-Yates shuffle in O(n) — std::random_shuffle removed in C++17
std::vector<int> deck(52);
std::iota(deck.begin(), deck.end(), 0);
std::shuffle(deck.begin(), deck.end(), rng); // C++11
// Sample 5 distinct elements without replacement (C++17)
std::vector<int> hand(5);
std::sample(deck.begin(), deck.end(), hand.begin(), 5, rng);Random Element
// Requires non-empty container; works with any random-access range
template<typename Container>
const auto& random_element(const Container& c, std::mt19937& rng) {
assert(!c.empty());
std::uniform_int_distribution<std::ptrdiff_t> dist(0, std::ssize(c) - 1); // std::ssize: C++20
return c[dist(rng)];
}Simulation Pattern
// Monte Carlo π estimate
std::mt19937_64 rng(std::random_device{}());
std::uniform_real_distribution<double> unit(-1.0, 1.0);
long long inside = 0;
const long long N = 10'000'000; // digit separator: C++14
for (long long i = 0; i < N; ++i) {
double x = unit(rng), y = unit(rng);
if (x*x + y*y <= 1.0) ++inside;
}
double pi = 4.0 * static_cast<double>(inside) / N;Thread Safety
Engines are not thread-safe. Sharing a single engine across threads with external locking works but serializes generation, which is usually the bottleneck. The idiomatic approach is per-thread engines:
// Seeded independently per thread — no contention (C++11 thread_local)
thread_local std::mt19937 thread_rng = [](){
std::seed_seq seq{
std::random_device{}(), std::random_device{}(),
std::random_device{}(), std::random_device{}()
};
return std::mt19937(seq);
}();
// Use directly without locks
int value = std::uniform_int_distribution<int>{1, 100}(thread_rng);Do not seed each thread with thread_id or a sequential integer — MT seeds that differ by one bit can produce correlated initial sequences.
Common Pitfalls
Constructing distributions inside tight loops: Distribution construction may involve a non-trivial setup (especially discrete_distribution). Construct once, call operator() repeatedly.
Seeding mt19937 from time(nullptr): time_t is often a 32-bit value and changes only once per second — trivially collide-able across processes started in the same second. Use random_device.
Using rand() % N: Produces modulo bias unless RAND_MAX + 1 is a multiple of N, and rand's underlying LCG has poor high-bit quality. rand() is deprecated in C++14 and may be removed.
default_random_engine in portable code: The standard does not specify which engine backs default_random_engine. Identical code produces different sequences on GCC, Clang, and MSVC — never use it when reproducibility matters.
random_device as a high-throughput generator: On Linux it reads from /dev/urandom; on Windows it calls BCryptGenRandom — both involve system calls with measurable overhead at scale. Use it only for seeding.
Best Practices
- Use
mt19937for 32-bit output,mt19937_64when generating values that span 64-bit ranges or when calling shuffle on large containers. - Seed with
seed_seqfrom multiplerandom_devicecalls when seed quality matters (simulations, games with replayability requirements). - Keep a fixed seed (
mt19937{42}) in unit tests — stochastic tests that fail intermittently are harder to debug than deterministic ones. - Prefer
thread_localengines to locking a single global engine. - Reuse distribution objects; their internal parameters are cheap to hold.
See Also
std::shuffle— Fisher-Yates permutation using a URBGstd::sample— reservoir sampling without replacement (C++17)std::ranges::shuffle— ranges overload (C++20)- Pseudo-random number engines — engine adaptor types (
shuffle_order_engine,discard_block_engine)