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.
Seeding Correctly: std::random_device and std::seed_seq
Claim. A single rd() call yields only 32 bits of entropy, leaving the remaining 19,905 bits of mt19937 state at their default values — sufficient for most programs but not for simulations where seed correlation matters.
// C++11
#include <random>
// Basic seeding — 32 bits of entropy, fast and fine for general use
std::random_device rd;
std::mt19937 gen(rd());
// Full 624-word state initialization — eliminates seed-correlation artefacts
std::seed_seq seed{ rd(), rd(), rd(), rd(), rd(), rd(), rd(), rd() };
std::mt19937 gen_full(seed);On some platforms (MinGW before recent fixes, certain embedded libc implementations) std::random_device is deterministic — rd.entropy() returns 0.0 as a hint. The standard does not require a non-zero return even for a genuine source, so treat it as advisory only and fall back to a time-based XOR when entropy is suspect.
// C++11 — fallback seeding when random_device is deterministic
#include <random>
#include <chrono>
std::mt19937 make_rng() {
std::random_device rd;
// Combine random_device with high-resolution clock to add variation
// even on platforms where rd() is constant.
auto time_bits = static_cast<uint32_t>(
std::chrono::high_resolution_clock::now().time_since_epoch().count()
);
std::seed_seq seed{
rd(), rd(), rd(), rd(),
time_bits,
time_bits ^ 0xDEADBEEFu
};
return std::mt19937(seed);
}Prefer seed_seq over raw arithmetic on entropy words — it applies a mixing function so that similar seed inputs produce uncorrelated engine states.
Distribution Reference: Uniform, Normal, Discrete
Claim. The standard library ships every common statistical distribution; picking the right one avoids hand-rolling error-prone transformations.
// C++11
#include <random>
std::mt19937 gen(std::random_device{}());
// Six-sided die: [1, 6] inclusive
std::uniform_int_distribution<int> d6(1, 6);
int roll = d6(gen);
// Floating-point uniform on [0.0, 1.0) — half-open on the right
std::uniform_real_distribution<double> unit(0.0, 1.0);
double u = unit(gen);
// Gaussian: mean=0, stddev=1
std::normal_distribution<double> gaussian(0.0, 1.0);
double z = gaussian(gen);
// Coin flip with bias p=0.6
std::bernoulli_distribution coin(0.6);
bool heads = coin(gen);
// Poisson: expected 4.5 events per interval
std::poisson_distribution<int> arrivals(4.5);
int n = arrivals(gen);
// Exponential: rate λ=2 (mean inter-arrival = 0.5 s)
std::exponential_distribution<double> wait(2.0);
double t = wait(gen);Construct distribution objects once per parameter set and call operator() repeatedly — construction can carry non-trivial setup cost, especially for discrete_distribution and piecewise_* families.
Thread-Safe Random Number Generation
Claim. Engines are stateful objects with no internal synchronisation; sharing one across threads requires a mutex, which turns random-number generation into a serialisation point.
// C++11 — per-thread engine: zero contention, no locks
#include <random>
#include <thread>
// Each thread gets its own fully-seeded engine on first use.
thread_local std::mt19937 tl_gen = []() {
std::random_device rd;
// XOR thread id into the seed to prevent all threads starting identically
// when random_device is deterministic.
auto tid = static_cast<uint32_t>(
std::hash<std::thread::id>{}(std::this_thread::get_id())
);
std::seed_seq seed{ rd(), rd(), rd(), rd(), tid };
return std::mt19937(seed);
}();
// Call from any thread without synchronisation:
int value = std::uniform_int_distribution<int>{1, 100}(tl_gen);The alternative — a shared engine protected by a mutex — is correct but contended in tight loops:
// C++11 — shared engine with mutex (correct, but serialised)
#include <random>
#include <mutex>
namespace {
std::mt19937 shared_gen(std::random_device{}());
std::mutex gen_mutex;
}
int thread_safe_roll() {
std::lock_guard<std::mutex> lock(gen_mutex);
return std::uniform_int_distribution<int>{1, 6}(shared_gen);
}std::rand() is not thread-safe in all standard library implementations — the C standard only requires thread-safety if _POSIX_THREAD_SAFE_FUNCTIONS is defined; avoid it entirely in multi-threaded code.
Reproducible Sequences: Seeding with a Fixed Value
Claim. Seeding with a compile-time constant guarantees identical output across runs on the same platform, which is invaluable for reproducible unit tests and deterministic simulations.
// C++11
#include <random>
#include <sstream>
std::mt19937 gen(42); // always produces the same sequence
// Advance n steps without drawing values — useful for splitting sequences
gen.discard(1000);
// Save generator state to a string
std::stringstream ss;
ss << gen;
std::string saved_state = ss.str();
// Restore state exactly
std::mt19937 gen2;
std::istringstream(saved_state) >> gen2;
// gen2 will now produce the same sequence as gen at the saved pointThe Mersenne Twister algorithm is specified by ISO C++, so mt19937 produces the same sequence for the same seed on all conforming implementations — cross-platform reproducibility is guaranteed for mt19937 and mt19937_64, but not for default_random_engine or minstd_rand. State serialisation via stream operators is the standard mechanism for checkpointing simulations.
Non-Uniform Integer Distributions: discrete, piecewise
Claim. Probability tables with non-uniform weights appear throughout game AI, load generation, and simulation; std::discrete_distribution encodes them directly without manual CDF construction.
// C++11
#include <random>
#include <vector>
std::mt19937 gen(std::random_device{}());
// Weighted integer sampling — weights need not sum to 1
// P(0)=1/6, P(1)=2/6, P(2)=3/6
std::discrete_distribution<int> weighted({1.0, 2.0, 3.0});
int choice = weighted(gen); // 0, 1, or 2
// Dynamic weights built at runtime (e.g. from a probability table)
std::vector<double> probs = {0.05, 0.10, 0.60, 0.20, 0.05};
std::discrete_distribution<int> table(probs.begin(), probs.end());
int tier = table(gen);For continuous ranges with piecewise-constant or piecewise-linear densities, use the higher-order distributions:
// C++11 — piecewise_constant: uniform density within each interval
// Intervals [0,1), [1,2), [2,3) with relative densities 1, 3, 1
std::vector<double> bounds = {0.0, 1.0, 2.0, 3.0};
std::vector<double> weights = {1.0, 3.0, 1.0};
std::piecewise_constant_distribution<double> pcd(bounds.begin(), bounds.end(), weights.begin());
double x = pcd(gen);
// C++11 — piecewise_linear: density interpolated between boundary values
std::vector<double> densities = {0.0, 1.0, 0.5, 0.0}; // one per boundary point
std::piecewise_linear_distribution<double> pld(bounds.begin(), bounds.end(), densities.begin());
double y = pld(gen);These distributions are particularly useful for modelling realistic request patterns in load tests or loot-table probabilities in game AI without hard-coding a manual alias table.
Generating Shuffles and Samples
Claim. std::shuffle supersedes the removed std::random_shuffle and requires an explicit URBG argument, making the randomness source visible and controllable.
// C++11
#include <random>
#include <algorithm>
#include <numeric>
#include <vector>
std::mt19937 gen(std::random_device{}());
// Build a 52-card deck and shuffle it — Fisher-Yates in O(n)
std::vector<int> deck(52);
std::iota(deck.begin(), deck.end(), 0);
std::shuffle(deck.begin(), deck.end(), gen); // C++11
// Deal 5 cards from the top
std::vector<int> hand(deck.begin(), deck.begin() + 5);std::random_shuffle (removed in C++17) used an internal source that could call std::rand, offering no control over seeding or distribution quality. Always pass an explicit URBG.
// C++17 — reservoir sampling: draw n items uniformly without replacement
#include <random>
#include <algorithm>
#include <vector>
#include <iterator>
std::mt19937 gen(std::random_device{}());
std::vector<int> population(1000);
std::iota(population.begin(), population.end(), 0);
std::vector<int> sample;
sample.reserve(10);
std::sample(population.begin(), population.end(),
std::back_inserter(sample), 10, gen); // C++17
// sample now holds 10 distinct elements chosen uniformlystd::sample works on input-iterator ranges (single-pass) using reservoir sampling when the source is not random-access, and is O(n) in all cases. It does not require the source range to fit in memory beyond the current element — suitable for large or lazily-generated ranges.
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)