C++ Random


  • Description: A note on <random> — engines, distributions, seeding, common patterns, and why rand() is not enough
  • My Notion Note ID: K2A-B1-24
  • Created: 2018-06-15
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Why Not rand()?

rand() (from <cstdlib>) is a holdover from C. Avoid it for any serious work:

  1. Implementation-defined quality. Some implementations have terrible statistical properties.
  2. Limited range. RAND_MAX is only guaranteed to be at least 32767 — too small for many uses.
  3. rand() % n is biased unless n divides RAND_MAX + 1.
  4. Single global state. Not thread-safe, and seeding affects the whole program.
  5. Not reproducible across implementations.

Use <random> (C++11) instead.


2. Engines and Distributions

<random> separates two concerns:

  1. Engine — the source of raw randomness. Produces uniformly distributed integers.
  2. Distribution — shapes engine output into the desired distribution (uniform real, normal, Bernoulli, etc.).
#include <random>

std::random_device rd;                       // OS-provided entropy (slow, high-quality)
std::mt19937 gen{rd()};                      // engine, seeded from rd

std::uniform_int_distribution<int> dist{1, 6};  // dice roll
int roll = dist(gen);

Engines you'll usually pick from:

Engine Quality Speed Use for
std::mt19937 Good Fast General-purpose 32-bit
std::mt19937_64 Good Fast General-purpose 64-bit
std::random_device Implementation-defined; usually OS entropy (may be deterministic on some implementations) Slow Seeding only, not bulk generation
std::minstd_rand Mediocre Very fast Speed-critical, low-stakes

Don't generate large amounts of randomness from random_device — it's typically backed by /dev/urandom or similar and is meant for seeding.


3. Common Patterns

#include <random>
#include <vector>
#include <algorithm>

// One-shot setup (avoid reseeding per call)
static std::mt19937 gen{std::random_device{}()};

// Uniform integer in [min, max], INCLUSIVE
int dice() {
    std::uniform_int_distribution<int> dist{1, 6};
    return dist(gen);
}

// Uniform real in [min, max)
double pct() {
    std::uniform_real_distribution<double> dist{0.0, 1.0};
    return dist(gen);
}

// Pick a random element
template <typename T>
const T& pick(const std::vector<T>& v) {
    std::uniform_int_distribution<size_t> dist{0, v.size() - 1};
    return v[dist(gen)];
}

// Shuffle
std::vector<int> v = {1, 2, 3, 4, 5};
std::shuffle(v.begin(), v.end(), gen);

// Coin flip
std::bernoulli_distribution coin{0.5};
bool heads = coin(gen);

4. Seeding

// Seed from OS entropy (recommended)
std::mt19937 gen{std::random_device{}()};

// Seed from a fixed value (reproducible — for tests, replays)
std::mt19937 gen{42};

// Seed with a sequence of values for better state
std::random_device rd;
std::seed_seq seed{rd(), rd(), rd(), rd()};   // 128 bits of entropy
std::mt19937 gen{seed};

Reproducibility: with the same seed and the same engine, you'll get the same sequence — across platforms, even. Distributions, however, are not required to be cross-platform consistent: two implementations may map the same engine output to different real numbers. Don't rely on cross-platform reproducibility of uniform_real_distribution results.


5. Distributions Reference

Distribution Use for
uniform_int_distribution Dice, array indices, etc.
uniform_real_distribution Continuous uniform (e.g. percentages)
bernoulli_distribution Coin flip with probability p
binomial_distribution Number of successes in n trials
poisson_distribution Count of events in fixed time
normal_distribution Gaussian (mean, stddev)
lognormal_distribution Lognormal
exponential_distribution Time between Poisson events
discrete_distribution Weighted choice from a list
geometric_distribution, negative_binomial_distribution Discrete
gamma_distribution, weibull_distribution, student_t_distribution, chi_squared_distribution, etc. Statistical sampling

6. Thread Safety

<random> engines are not thread-safe. Sharing one across threads requires external synchronization, which destroys throughput.

The standard pattern: one engine per thread, seeded independently.

thread_local std::mt19937 tls_gen{
    std::random_device{}() +
    std::hash<std::thread::id>{}(std::this_thread::get_id())
};

int dice() {
    std::uniform_int_distribution<int> dist{1, 6};
    return dist(tls_gen);
}

For most use cases this gives near-perfect scaling and avoids the need for any locking.