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.

See Also