C++ random | Engines, distributions, and replacing rand()
Key takeaways
C++11 random: random_device seeding, mt19937, uniform and normal distributions, shuffle, weighted picks, threading, and when to use a cryptographic RNG instead.
What is Random?
The C++11 <random> library is a standard library for high-quality random number generation. More powerful and flexible than C-style rand().
#include <random>
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<> dist{1, 6};
int dice = dist(gen); // 1-6
Why do you need it?:
- Quality: Ensure equal distribution
- Flexibility: Supports various distributions
- Reproducibility: Reproduce results with seeds
- Type safe: template based
// ❌ rand(): not uniform, predictable
int r = rand() % 100; // 0-99, but not evenly distributed
// ✅ C++11 random: uniform, high quality
std::mt19937 gen{std::random_device{}()};
std::uniform_int_distribution<> dist{0, 99};
int r = dist(gen); // 0-99, evenly distributed
The “not uniform” problem with rand() % 100 is concrete, not theoretical: RAND_MAX is implementation-defined and often 32767, which isn’t evenly divisible by 100 — the values near the top of rand()’s range end up mapped to a smaller slice of the % 100 output than the rest, so some remainders are very slightly but measurably more likely than others. uniform_int_distribution handles this correctly internally (it doesn’t just apply % to the engine’s raw output), which is the actual quality difference the “uniform” label refers to.
Random Structure:
flowchart LR
A[random_device] -->|seed| B[engine: mt19937]
B -->|raw random bits| C[distribution: uniform_int_distribution]
C -->|final value| D[result]
3-step structure:
| steps | Role | Example |
|---|---|---|
| 1. Seed | Create initial value | std::random_device rd; |
| 2. Engine | random number generation | std::mt19937 gen{rd()}; |
| 3. Distribution | Range adjustment | std::uniform_int_distribution<> dist{0, 99}; |
// 1. Seed: hardware entropy
std::random_device rd;
// 2. Engine: Mersenne Twister
std::mt19937 gen{rd()};
// 3. Distribution: uniform over 0-99
std::uniform_int_distribution<> dist{0, 99};
// 4. Generate
int random = dist(gen);
Splitting random number generation into these three separate stages, instead of one random() call like C’s rand(), is what makes the library composable: the same mt19937 engine can feed a uniform_int_distribution for dice rolls, a normal_distribution for simulated measurement noise, and a bernoulli_distribution for a coin flip, all from one seeded source of entropy. The engine’s job is only to produce a long, statistically well-distributed stream of raw bits — turning that stream into “a number between 1 and 6” or “true 20% of the time” is the distribution’s job, and separating the two lets you swap either independently.
Engine plus distribution
#include <random>
// 1. Seed
std::random_device rd;
// 2. Engine
std::mt19937 gen{rd()};
// 3. Distribution
std::uniform_int_distribution<> dist{0, 99};
// 4. Generate
int random = dist(gen);
Dice, ranged integers, shuffles, and weighted selection
A dice class that seeds once
#include <random>
class Dice {
std::mt19937 gen;
std::uniform_int_distribution<> dist;
public:
Dice() : gen{std::random_device{}()}, dist{1, 6} {}
int roll() {
return dist(gen);
}
};
int main() {
Dice dice;
for (int i = 0; i < 10; ++i) {
std::cout << dice.roll() << " ";
}
}
Storing gen and dist as member variables, constructed once, is the pattern to imitate — the constructor pays the cost of seeding from random_device exactly once, and every roll() call afterward is just cheap engine advancement plus a distribution lookup. Constructing a fresh mt19937 from random_device on every roll (a mistake covered later in this guide) would make dice-rolling dramatically slower for no quality benefit.
Random integers in a range
#include <random>
int randomInt(int min, int max) {
static std::random_device rd;
static std::mt19937 gen{rd()};
std::uniform_int_distribution<> dist{min, max};
return dist(gen);
}
double randomDouble(double min, double max) {
static std::random_device rd;
static std::mt19937 gen{rd()};
std::uniform_real_distribution<> dist{min, max};
return dist(gen);
}
int main() {
std::cout << randomInt(1, 100) << std::endl;
std::cout << randomDouble(0.0, 1.0) << std::endl;
}
The static on rd and gen here is doing real work: without it, every call to randomInt would construct a brand-new random_device and reseed a brand-new mt19937, both of which are more expensive than generating the actual random number. static makes the engine persist across calls within the same thread, seeded exactly once on first use — the dist object, by contrast, is cheap to construct fresh each call since its parameters (min, max) change per call anyway.
Shuffling with std::shuffle
#include <random>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> cards = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::random_device rd;
std::mt19937 gen{rd()};
std::shuffle(cards.begin(), cards.end(), gen);
for (int card : cards) {
std::cout << card << " ";
}
}
std::shuffle replaced the older std::random_shuffle (removed in C++17) specifically because random_shuffle relied on rand() internally by default, inheriting all of its quality and thread-safety problems. shuffle requires you to pass a proper UniformRandomBitGenerator like mt19937 explicitly — more typing, but it means the shuffle quality is exactly as good as whatever engine you choose to hand it, not silently tied to the C runtime’s rand().
Weighted selection
#include <random>
#include <vector>
int main() {
std::vector<int> items = {1, 2, 3, 4, 5};
std::vector<double> weights = {0.1, 0.2, 0.3, 0.2, 0.2};
std::random_device rd;
std::mt19937 gen{rd()};
std::discrete_distribution<> dist{weights.begin(), weights.end()};
// Pick indices according to the given weights
for (int i = 0; i < 10; ++i) {
int index = dist(gen);
std::cout << items[index] << " ";
}
}
discrete_distribution takes care of a detail that’s easy to get wrong writing this by hand: the weights here (0.1, 0.2, 0.3, 0.2, 0.2) happen to sum to 1.0, but they don’t need to — the distribution normalizes them internally, so {1, 2, 3, 2, 2} (unnormalized, same ratios) produces identical selection probabilities. That’s genuinely convenient when weights come from a source like raw item counts or scores that aren’t pre-normalized into probabilities.
Choosing an engine
// mt19937: Mersenne Twister (recommended default)
std::mt19937 gen;
// mt19937_64: 64-bit variant
std::mt19937_64 gen64;
// default_random_engine
std::default_random_engine gen_default;
// minstd_rand: linear congruential
std::minstd_rand gen_lcg;
default_random_engine is deliberately not recommended here despite the inviting name — the standard leaves its actual algorithm implementation-defined, so default_random_engine on one compiler/standard library can produce a completely different sequence (and different quality characteristics) than on another, even from the same seed. mt19937 is specified precisely enough that its output sequence is identical across any conforming implementation given the same seed, which matters if you ever need reproducible results across platforms — minstd_rand (a linear congruential generator) is faster but has known statistical weaknesses that make it unsuitable for anything beyond quick, non-critical uses.
Seeding, per-call engines, and rand()
A fixed seed
// ❌ Fixed seed
std::mt19937 gen{42}; // always the same sequence
// ✅ random_device
std::random_device rd;
std::mt19937 gen{rd()};
// ✅ time-based (not recommended)
std::mt19937 gen{static_cast<unsigned>(std::time(nullptr))};
A fixed seed isn’t a mistake in every context — it’s exactly what you want for reproducible tests or deterministic simulations where “the same random sequence every run” is the goal. It becomes a bug only when unintentional: shipping std::mt19937 gen{42} in production code that’s supposed to feel random produces the identical sequence of “random” events every time the program restarts, which is a real, recurring bug class in games and procedural generation. The time-based seed is listed as “not recommended” because it’s predictable and low-resolution — two processes started within the same second get the same seed, and an attacker who knows roughly when a process started has a small search space to guess the seed.
Constructing the engine on every call
// ❌ Constructing fresh every call
int random() {
std::random_device rd;
std::mt19937 gen{rd()}; // slow
std::uniform_int_distribution<> dist{0, 99};
return dist(gen);
}
// ✅ static
int random() {
static std::random_device rd;
static std::mt19937 gen{rd()};
static std::uniform_int_distribution<> dist{0, 99};
return dist(gen);
}
std::random_device reading from the OS’s entropy source (/dev/urandom on Linux, CryptGenRandom-family APIs on Windows) is genuinely slow relative to mt19937’s per-call cost — calling it on every single random number request, as the first version does, can dominate the runtime of a function that’s supposed to be cheap. The fix mirrors the ranged-integer example above: seed once, reuse the engine for the life of the program (or thread).
Rebuilding the distribution each call
std::mt19937 gen{std::random_device{}()};
// ❌ Recreating the distribution every iteration
for (int i = 0; i < 100; ++i) {
std::uniform_int_distribution<> dist{0, 99};
int r = dist(gen);
}
// ✅ Reuse the distribution
std::uniform_int_distribution<> dist{0, 99};
for (int i = 0; i < 100; ++i) {
int r = dist(gen);
}
This one is a smaller cost than reconstructing the engine (the previous mistake), since a distribution object is typically lightweight — but it’s still unnecessary work repeated 100 times for no benefit when the parameters (0, 99) never change across iterations. The general rule that emerges from Problems 2 and 3 together: construct the engine once per program/thread lifetime, and construct each distribution once per set of parameters, reusing both across as many calls as share those parameters.
Falling back to rand()
// ❌ rand() (C-style, not recommended)
int r = rand() % 100; // not evenly distributed
// ✅ C++11 random
std::mt19937 gen{std::random_device{}()};
std::uniform_int_distribution<> dist{0, 99};
int r = dist(gen);
Matching the distribution to the model
// Integer distributions
std::uniform_int_distribution<> uniform{0, 99};
std::binomial_distribution<> binomial{10, 0.5};
std::poisson_distribution<> poisson{4.0};
// Real-number distributions
std::uniform_real_distribution<> uniformReal{0.0, 1.0};
std::normal_distribution<> normal{0.0, 1.0};
std::exponential_distribution<> exponential{1.0};
// Boolean distribution
std::bernoulli_distribution bernoulli{0.7};
Picking the right distribution is really picking the right model for the thing you’re simulating, and the choice usually falls out of the domain rather than being arbitrary: normal_distribution for measurement-like quantities that cluster around a mean (heights, sensor noise), poisson_distribution for counting how many independent events happen in a fixed interval (customers arriving per hour), exponential_distribution for the time between such events (time until the next arrival), and bernoulli_distribution for a single weighted yes/no outcome. Reaching for uniform_real_distribution to approximate any of these instead would produce a shape of randomness that doesn’t match the real-world process being modeled, even though it compiles and runs without complaint.
Game randomness, sampling, and reproducible simulations
A game RNG class
class GameRandom {
std::mt19937 gen_;
public:
GameRandom() : gen_{std::random_device{}()} {}
// Dice roll
int rollDice(int sides = 6) {
std::uniform_int_distribution<> dist{1, sides};
return dist(gen_);
}
// Probability check (0.0 - 1.0)
double chance() {
std::uniform_real_distribution<> dist{0.0, 1.0};
return dist(gen_);
}
// Critical hit (20% chance)
bool criticalHit() {
std::bernoulli_distribution dist{0.2};
return dist(gen_);
}
};
// Usage
GameRandom rng;
int damage = rng.rollDice(20); // 1d20
if (rng.criticalHit()) {
damage *= 2;
}
Wrapping the engine in a class like this, with one shared gen_ feeding several purpose-specific methods, is the practical middle ground between the two extremes covered above: one global unprotected engine (unsafe across threads) and constructing a fresh engine per call (slow). Each GameRandom instance owns its own engine and distributions are created cheaply per call since their parameters vary by call site (sides for dice, fixed probabilities for chance/crit) — the expensive part, the seeded engine, is built exactly once per instance.
Sampling without replacement
#include <random>
#include <vector>
#include <algorithm>
template<typename T>
std::vector<T> randomSample(const std::vector<T>& data, size_t n) {
if (n >= data.size()) {
return data;
}
std::vector<T> result = data;
std::random_device rd;
std::mt19937 gen{rd()};
std::shuffle(result.begin(), result.end(), gen);
result.resize(n);
return result;
}
// Usage
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto sample = randomSample(data, 3); // pick 3 at random
Shuffle-then-truncate is a simple, correct way to get an unbiased random sample without replacement, but it’s worth knowing it’s not the most efficient approach for large data and small n — it does O(data.size()) work to shuffle the entire vector even when you only need 3 elements out of a million. std::sample (C++17, in <algorithm>) implements reservoir sampling internally and gets you the same unbiased result in O(data.size()) time but without the extra memory traffic of shuffling elements you’re about to discard — worth reaching for directly if you’re on C++17 or later.
A seedable simulation
#include <random>
class Simulation {
std::mt19937 gen_;
std::normal_distribution<> normal_;
std::exponential_distribution<> exponential_;
public:
Simulation(unsigned seed = std::random_device{}())
: gen_{seed},
normal_{0.0, 1.0}, // mean 0, standard deviation 1
exponential_{1.0} {} // rate (lambda) 1
// Normal distribution (height, weight, etc.)
double normalValue() {
return normal_(gen_);
}
// Exponential distribution (wait times, etc.)
double exponentialValue() {
return exponential_(gen_);
}
};
// Usage
Simulation sim;
double height = 170.0 + sim.normalValue() * 10.0; // mean 170cm
double waitTime = sim.exponentialValue(); // mean 1 second
The optional seed constructor parameter, defaulting to std::random_device{}(), is a small design choice worth copying into your own simulation code: it lets production code call Simulation sim; for genuine randomness while tests call Simulation sim(12345); for a fixed, reproducible sequence — the same class serves both needs without any conditional logic inside it, just a default argument.
FAQ
Q1: What is Random?
A: C++11 random number generation library. Generates high-quality random numbers and supports various distributions.
#include <random>
std::mt19937 gen{std::random_device{}()};
std::uniform_int_distribution<> dist{0, 99};
int r = dist(gen);
Q2: What is the structure?
A: Seed + Engine + Distribution 3-stage structure.
std::random_device rd; // 1. seed
std::mt19937 gen{rd()}; // 2. engine
std::uniform_int_distribution<> dist; // 3. distribution
int r = dist(gen); // 4. generate
Q3: Which engine should I use?
A: std::mt19937 (Mersenne Twister) is recommended.
std::mt19937 gen{std::random_device{}()}; // 32-bit
std::mt19937_64 gen64{std::random_device{}()}; // 64-bit
Q4: Can’t I use rand()?
A: Not recommended. rand() has issues with uneven distribution, low quality, and global state.
// ❌ rand(): not evenly distributed
int r = rand() % 100;
// ✅ C++11 random: evenly distributed
std::uniform_int_distribution<> dist{0, 99};
int r = dist(gen);
Q5: How do I set the seed?
A: std::random_device is recommended.
// ✅ random_device: hardware entropy
std::random_device rd;
std::mt19937 gen{rd()};
// ❌ time-based: predictable
std::mt19937 gen{static_cast<unsigned>(std::time(nullptr))};
Q6: How to generate reproducible random numbers?
A: Uses a fixed seed.
// Reproducible
std::mt19937 gen{42}; // always the same sequence
// Useful for tests
void testRandom() {
std::mt19937 gen{12345};
// Always the same result
}
Q7: Is it thread safe?
A: Not thread-safe. A separate engine must be used for each thread.
// ❌ Global engine: race condition
std::mt19937 globalGen;
void threadFunc() {
int r = std::uniform_int_distribution<>{0, 99}(globalGen); // unsafe
}
// ✅ thread_local
thread_local std::mt19937 gen{std::random_device{}()};
void threadFunc() {
int r = std::uniform_int_distribution<>{0, 99}(gen); // safe
}
Q8: What are Random learning resources?
A:
- “C++ Primer” by Stanley Lippman
- “Effective Modern C++” by Scott Meyers
- cppreference.com - Random
C++11 random is a standard library for high-quality random number generation.
Related Articles
- C++ Distribution | Probability Distribution Guide
- C++ Random Number Generation | ‘random’ Library Guide
- C++ random_device | Hardware Random Numbers Guide
- std::async and Launch Policies
- C++ std::atomic