- 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() (<cstdlib>) is a C holdover. Avoid for serious work:
- Implementation-defined quality — some impls have terrible statistical properties.
- Limited range —
RAND_MAX guaranteed only ≥ 32767. Too small for many uses.
rand() % n biased unless n divides RAND_MAX + 1.
- Single global state — not thread-safe; seeding affects whole program.
- Not reproducible across implementations.
2. Engines and Distributions
<random> separates 2 concerns:
- Engine — source of raw randomness. Produces uniformly distributed integers.
- Distribution — shapes engine output into desired distribution (uniform real, normal, Bernoulli, etc.).
#include <random>
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<int> dist{1, 6};
int roll = dist(gen);
Engines:
| 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 bulk randomness from
random_device — typically backed by /dev/urandom; meant for seeding.
3. Common Patterns
#include <random>
#include <vector>
#include <algorithm>
static std::mt19937 gen{std::random_device{}()};
int dice() {
std::uniform_int_distribution<int> dist{1, 6};
return dist(gen);
}
double pct() {
std::uniform_real_distribution<double> dist{0.0, 1.0};
return dist(gen);
}
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)];
}
std::vector<int> v = {1, 2, 3, 4, 5};
std::shuffle(v.begin(), v.end(), gen);
std::bernoulli_distribution coin{0.5};
bool heads = coin(gen);
4. Seeding
std::mt19937 gen{std::random_device{}()};
std::mt19937 gen{42};
std::random_device rd;
std::seed_seq seed{rd(), rd(), rd(), rd()};
std::mt19937 gen{seed};
- Reproducibility: same seed + same engine → same sequence (cross-platform).
- But distributions are not required to be cross-platform consistent — two impls may map same engine output to different reals. Don't rely on cross-platform
uniform_real_distribution reproducibility.
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
- Engines are not thread-safe. Sharing across threads → needs sync → kills throughput.
- 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);
}
- Near-perfect scaling, no locking needed for most cases.