Skip to content
C++
Library
since C++11
Basic

Random Number Generation

C++ <random> engines, distributions, seeding strategies, thread safety, and common pitfalls — from mt19937 to discrete_distribution.

<random>since C++11

The <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) or std::mt19937_64 (64-bit) for virtually all non-cryptographic uses.
  • Seeding: std::random_device for entropy, optionally fed through std::seed_seq to 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

cpp
#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:

cpp
// 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

cpp
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

cpp
// [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++20

Normal (Gaussian)

cpp
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

cpp
std::bernoulli_distribution fair(0.5);
std::bernoulli_distribution biased(0.7);    // true 70% of the time

bool outcome = fair(rng);

Discrete (Weighted Choice)

cpp
// 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

cpp
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

cpp
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

cpp
#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

cpp
// 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

cpp
// 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:

cpp
// 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 mt19937 for 32-bit output, mt19937_64 when generating values that span 64-bit ranges or when calling shuffle on large containers.
  • Seed with seed_seq from multiple random_device calls 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_local engines 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.

cpp
// 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.

cpp
// 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.

cpp
// 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.

cpp
// 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:

cpp
// 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.

cpp
// 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 point

The 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.

cpp
// 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:

cpp
// 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.

cpp
// 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.

cpp
// 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 uniformly

std::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