C++ Random Engines and Distributions

Separate C++ random engines from distributions, seed deliberately, test repeatability across stated environments, and sample without replacement.

Lesson 36 of 44Intermediate10 min read

A random engine and a distribution solve different problems. The engine advances a state to produce a pseudorandom sequence. The distribution maps those values into the range or probability model the feature needs. A dice simulation needs a uniform integer range; a repeatable test needs a deliberate seed and controlled ownership of the state.

This guide uses C++17. You will verify engine repeatability, check distribution bounds, and draw a small sample without replacement. These facilities are appropriate for simulations and ordinary randomized algorithms. They are not a substitute for a cryptographic random source when generating credentials, session tokens or other security-sensitive secrets.

The engine owns evolving state

An engine such as mt19937 stores state that advances on each call. A fixed seed gives a repeatable engine sequence under its specified algorithm. Copying an engine copies its current state, so two copies can then produce matching future engine values when advanced equally.

Create and seed the engine at a meaningful boundary, then pass it by reference to operations that consume it. Reseeding before every draw can restart the sequence and destroy the intended behavior. Copying the engine accidentally into each helper can similarly repeat the same values. Keep ownership clear: one simulation run can own one engine, while independent concurrent runs can own separate engines. A shared engine needs synchronization if accessed concurrently.

A distribution expresses the required probability model

Uniform_int_distribution maps an engine into an inclusive integer interval. The lower bound must not exceed the upper bound. Other distributions represent different models, such as normal variation or a Bernoulli outcome. Select a distribution from the feature's meaning rather than forcing every requirement into a raw modulo operation.

Using engine output modulo a range can introduce bias when the engine's possible outcomes do not divide evenly by that range. A standard distribution handles the mapping under its contract. Do not infer statistical quality from a handful of visually varied numbers. A range test proves bounds, not uniformity. Statistical tests require an appropriate sample and interpretation, while ordinary unit tests should focus on the deterministic feature behavior around the random dependency.

Fixed seeds do not make every mapped sequence portable

A specified engine such as mt19937 has a defined algorithm, but standard distribution algorithms can differ between library implementations. The same engine seed can therefore produce different distributed values on different standard libraries. Std::shuffle can also produce implementation-dependent permutations.

For tests, verify invariants such as bounds, sample size and uniqueness unless exact output is part of an explicitly controlled algorithm contract. If a simulation must reproduce every result across implementations, define and version the complete mapping algorithm and environment, not merely the seed. Default_random_engine is itself implementation-defined, so its name does not provide a cross-platform engine identity.

Seeding and security have different requirements

Random_device can supply seed material from a nondeterministic source when supported, but implementations can fall back to a deterministic engine under documented limitations. It can also fail. Do not assume its name alone guarantees a cryptographic source on every platform.

For a simulation, a recorded fixed seed is valuable because a failing run can be reproduced. For varied ordinary runs, a reviewed seeding strategy can initialize a long-lived engine. For secrets, use the platform or library's supported cryptographic randomness API under its security contract. Mt19937 is predictable from state and is unsuitable for secret tokens even if initially seeded with strong entropy.

Inject randomness into testable behavior

A feature can receive an engine or a small random-choice interface instead of constructing global random state internally. Tests can then use a known seed or a deterministic fake to exercise boundary decisions. Keep the abstraction focused on the feature's need, such as selecting a valid index.

Do not test randomness by requiring every short sample to contain every possible value. That can fail legitimately. Test zero-size behavior, full-size samples, invalid counts and ownership. For a weighted selector, validate weights and test the deterministic mapping boundaries separately from statistical behavior. Recording a safe seed and environment can help diagnose simulation failures without retaining confidential input.

Define bounds before requesting a draw

An empty collection has no valid element index, so constructing a distribution over zero through size minus one without checking emptiness is wrong. A range supplied by a caller must be validated before it reaches the distribution. Integer types and conversion rules still apply to size arithmetic.

A randomized algorithm should also have a bounded amount of work. Repeatedly drawing until enough distinct values appear can become slow near the full population size. Shuffling a bounded collection or using a suitable sampling algorithm provides a clearer termination argument. Choose the method from population size, memory limits and the required probability contract.

Official references

C++ Random Engines and Distributions Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Verify repeatability and bounds

This complete program prints a known initial mt19937 engine value and checks two equally seeded distribution workflows within the same implementation. It does not assert a portable sequence of dice faces.

The checks exercise the stated contracts without claiming that a finite sample proves distribution quality. Both engines and distributions are local to this operation.

C++
#include <iostream>
#include <random>

int main() {
    std::mt19937 raw(5489u);
    std::cout << "First engine value: " << raw() << '\n';

    std::mt19937 left(42u);
    std::mt19937 right(42u);
    std::uniform_int_distribution<int> left_die(1, 6);
    std::uniform_int_distribution<int> right_die(1, 6);

    bool identical = true;
    bool bounded = true;
    for (int trial = 0; trial < 100; ++trial) {
        int first = left_die(left);
        int second = right_die(right);
        identical = identical && first == second;
        bounded = bounded && first >= 1 && first <= 6;
    }

    std::cout << std::boolalpha;
    std::cout << "Repeatable here: " << identical << '\n';
    std::cout << "Within bounds: " << bounded << '\n';
}
Output
First engine value: 3499211612
Repeatable here: true
Within bounds: true

C++ Random Engines and Distributions in Real Projects

Worked exercise: choose distinct jobs

The task is to choose three distinct jobs from a known unique list. Copy the owned input, shuffle it using a caller-owned engine and resize the copy to the requested count. This is sampling without replacement. The source remains unchanged, and an excessive requested count is rejected.

C++
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <random>
#include <set>
#include <stdexcept>
#include <vector>

std::vector<int> choose_jobs(const std::vector<int>& source,
                            std::size_t count,
                            std::mt19937& engine) {
    if (count > source.size())
        throw std::invalid_argument("sample too large");
    std::vector<int> chosen = source;
    std::shuffle(chosen.begin(), chosen.end(), engine);
    chosen.resize(count);
    return chosen;
}

int main() {
    const std::vector<int> jobs{10, 20, 30, 40, 50};
    std::mt19937 engine(7u);
    auto chosen = choose_jobs(jobs, 3, engine);
    std::set<int> distinct(chosen.begin(), chosen.end());
    std::cout << "Selected: " << chosen.size() << '\n';
    std::cout << std::boolalpha
              << "Distinct: " << (distinct.size() == chosen.size()) << '\n';
    std::cout << "Source size: " << jobs.size() << '\n';
}
Output
Selected: 3
Distinct: true
Source size: 5

The exact selected IDs can vary across shuffle implementations, so the output verifies the feature's invariants. If the source contained duplicate IDs, shuffling alone would not guarantee distinct logical jobs; validate or deduplicate that input under a separate policy.

C++ Random Engines and Distributions FAQ

Is mt19937 suitable for passwords or session tokens?

No. It is a pseudorandom engine for ordinary algorithms and simulations, not a cryptographic secret generator. Its predictable state behavior is useful for reproducibility and harmful for credentials. Strong seed material does not transform its algorithm into a secure token source. Use a supported cryptographic API with an explicit platform and library security contract.

Why not seed before every random value?

Reseeding resets or replaces the evolving state and can repeat values or reduce the intended sequence behavior. Seed at the start of a run, then consume the engine by reference. Tests can deliberately restart an engine to reproduce a run. Production ownership should make accidental reseeding or copying difficult, especially when several helpers participate in one simulation.

Will a fixed seed reproduce dice values on every compiler?

A specified engine sequence can be defined, but distributions and shuffling can use different allowed algorithms across standard libraries. For exact cross-environment reproduction, control the complete mapping and implementation contract. Ordinary tests should assert range and feature invariants. Record the seed together with the environment when investigating a simulation result.

Does shuffling guarantee distinct sampled values?

It preserves the input elements without replacement at the element-position level. If the input already contains duplicate logical values, the sample can contain duplicates. The worked exercise assumes unique job IDs and verifies that condition in its fixture. A general API should validate uniqueness or define whether duplicates represent separate selectable entries.

C++ Random Engines and Distributions Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A selector needs three distinct IDs from a unique bounded list. Which approach has a clear termination argument?

C++ Random Engines and Distributions Quiz

2 questions to check what you learned.

  1. What role does a distribution play?

    • It owns every application's global seed.
    • It automatically encrypts generated values.
    • It maps engine output into a chosen probability model or range.
    • It makes every short sample contain every possible outcome.
  2. Why can exact dice output differ across standard libraries with the same mt19937 seed?

    • A conforming distribution may emit values outside its declared closed interval.
    • The engine sequence is unspecified even for its documented deterministic seed constructor.
    • Using the same seed forces every distribution implementation to use the same mapping algorithm.
    • Distribution mapping algorithms can differ while satisfying their contract.