Random Number Generation in C++

by | C++, Programming

Today, we’ll explore how to generate random numbers effectively in C++, with a focus on modern approaches and best practices. Whether you’re developing games, running simulations, or working with statistical analysis, understanding random number generation is crucial for writing robust and reliable code.

Introduction

Random number generation is a fundamental concept in programming, with applications ranging from simple games to complex cryptographic systems. In C++, we have several approaches available, but the modern <random> library introduced in C++11 provides the most robust and flexible solution for generating high-quality random numbers.

📚 Random Number Generation Quick Reference
std::random_device
A uniformly-distributed integer random number generator that produces non-deterministic random numbers from a hardware source when available.
std::mt19937
A Mersenne Twister pseudo-random generator with a period of 2^19937-1, providing high-quality random numbers for non-cryptographic use.
Distribution
A mathematical function that describes the probability of different random outcomes occurring, like uniform, normal, or binomial distributions.
Seed
An initial value used to start a pseudo-random number sequence. The same seed will always produce the same sequence of numbers.
PRNG
Pseudo-Random Number Generator – An algorithm that generates a sequence of numbers that approximate the properties of random numbers.
Entropy Pool
A source of environmental randomness used by std::random_device to generate true random numbers, often from hardware events.

Modern Approach (C++11)

The C++11 <random> library introduces a two-step approach to random number generation: first, you create a random number engine, then you apply a distribution to shape the output according to your needs. This approach allows you to generate high-quality random numbers with a wide variety of distributions. Let’s break it down step by step:

Basic Random Number Generation
#include <iostream>
#include <random>

int main() {
    // Step 1: Create a random number generator engine
    std::random_device rd;      // Non-deterministic source of random numbers
    std::mt19937 gen(rd());     // Mersenne Twister engine seeded with random_device

    // Step 2: Define a distribution
    std::uniform_int_distribution<int> dis(1, 6); // Uniform distribution between 1 and 6

    // Step 3: Generate random numbers using the engine and distribution
    std::cout << "Rolling a die 5 times:" << std::endl;
    for (int i = 0; i < 5; ++i) {
        std::cout << dis(gen) << " "; // Output random numbers
    }
    std::cout << std::endl;

    return 0;
}
Rolling a die 5 times:
4 2 6 1 3

Explanation

The code above demonstrates the modern approach to random number generation:

  • Step 1: Create a Random Number Engine
    The std::random_device is used as a source of non-deterministic randomness to seed the std::mt19937 engine. This ensures high-quality randomness.
  • Step 2: Define a Distribution
    The std::uniform_int_distribution ensures that the numbers generated are evenly distributed between 1 and 6 (inclusive). This is ideal for simulating a die roll.
  • Step 3: Generate Random Numbers
    The dis(gen) generates random numbers by combining the engine with the distribution.

Interactive Example

Want to see dice rolling in action? Check out our Interactive Dice Roller. It uses the same principles to simulate dice rolls in real time!

💡 Pro Tip: Always seed your random number engine with std::random_device for non-deterministic results. Avoid using hardcoded seeds unless you need reproducibility for testing.

Working with Distributions

The <random> library in C++ provides a wide variety of distributions to shape random number outputs according to specific probability models. This flexibility is essential for simulations, games, and statistical applications. Here’s a deeper look at how different distributions work:

Common Distributions

Below are some commonly used distributions and their typical use cases:

  • Uniform Distribution: Generates numbers with equal probability over a specified range. Useful for fair dice rolls or random sampling.
  • Normal Distribution: Follows a bell curve (Gaussian distribution) and is used in applications like noise simulation or modeling natural phenomena.
  • Bernoulli Distribution: Models binary outcomes, such as coin flips or success/failure scenarios.
Different Distribution Types
#include <iostream>
#include <random>
#include <iomanip>

int main() {
    // Step 1: Create a random number engine
    std::random_device rd;
    std::mt19937 gen(rd());

    // Step 2: Define different distributions
    std::uniform_real_distribution<double> real_dis(0.0, 1.0);  // Uniform (0.0 to 1.0)
    std::normal_distribution<double> normal_dis(0.0, 1.0);     // Normal (mean=0, stddev=1)
    std::bernoulli_distribution bern_dis(0.7);                     // Bernoulli (p=0.7)

    // Step 3: Generate random numbers
    std::cout << std::fixed << std::setprecision(4);
    std::cout << "Uniform: " << real_dis(gen) << std::endl;
    std::cout << "Normal:  " << normal_dis(gen) << std::endl;
    std::cout << "Bernoulli: " << bern_dis(gen) << std::endl;

    return 0;
}
Uniform: 0.6432
Normal: -0.2537
Bernoulli: 1

Explanation

The example code demonstrates the use of three popular distributions:

  • Uniform Real Distribution: Produces continuous values evenly distributed between the specified range (0.0 to 1.0). Commonly used for scenarios requiring equal probability of outcomes.
  • Normal Distribution: Simulates real-world randomness with a bell-shaped curve, characterized by its mean (center) and standard deviation (spread). For example, it can model physical measurements or financial returns.
  • Bernoulli Distribution: Outputs 1 or 0 based on the given probability (0.7 in this case). It’s ideal for binary processes like coin tosses or success/failure trials.

Other Available Distributions

C++ also includes a variety of additional distributions:

  • Exponential Distribution: Models the time between events in a Poisson process (e.g., waiting times).
  • Binomial Distribution: Represents the number of successes in a fixed number of trials.
  • Poisson Distribution: Models the number of events in a fixed interval, such as customer arrivals or decay events.
  • Discrete Distribution: Produces outcomes from a predefined set of probabilities.

💡 Pro Tip: Choose a distribution that aligns with your problem's requirements. For instance, use normal distribution for data resembling natural phenomena or uniform distribution for fair sampling.

Legacy Approach

Before C++11 introduced the <random> library, the rand() function from the C Standard Library was the go-to method for generating random numbers. While simple to use, it comes with significant limitations that make it unsuitable for many modern applications. Understanding its drawbacks is crucial if you encounter legacy codebases that still rely on rand().

Example of rand()

Legacy Random Generation (Not Recommended)
#include <iostream>
#include <cstdlib>
#include <ctime>

int main() {
    // Seed the random number generator with the current time
    srand(time(0));

    // Generate numbers between 1 and 6
    std::cout << "Rolling a die 5 times:" << std::endl;
    for (int i = 0; i < 5; ++i) {
        int roll = rand() % 6 + 1; // Generates numbers from 1 to 6
        std::cout << roll << " ";
    }
    std::cout << std::endl;

    return 0;
}
Rolling a die 5 times:
3 5 1 6 2

Limitations of rand()

Although simple, rand() suffers from several critical issues:

  • Poor Randomness: The quality of randomness is low, as the underlying algorithm often produces predictable patterns.
  • Small Range: The range of values is limited by RAND_MAX, which can be as low as 32767 on some systems.
  • Modulo Bias: Using the modulo operator (%) to restrict the range causes uneven distribution, particularly when the range does not evenly divide RAND_MAX.
  • No Distribution Support: rand() cannot generate numbers based on specific distributions, such as normal or exponential.
  • Global State: rand() uses a single global state, making it unsafe for multi-threaded applications.

Improving rand() (Still Not Recommended)

If you must use rand() in legacy systems, here are a few steps to mitigate its drawbacks:

Improving rand() with Better Seeding
#include <iostream>
#include <cstdlib>
#include <ctime>

int generate_random(int min, int max) {
    return min + rand() % (max - min + 1); // Avoids hardcoding range
}

int main() {
    // Seed the random number generator with the current time
    srand(static_cast<unsigned int>(time(0)));

    // Generate numbers between 1 and 6
    std::cout << "Rolling a die 5 times:" << std::endl;
    for (int i = 0; i < 5; ++i) {
        std::cout << generate_random(1, 6) << " ";
    }
    std::cout << std::endl;

    return 0;
}
Rolling a die 5 times:
4 2 6 3 5

While this approach improves readability and flexibility, it still inherits the core issues of rand(), such as poor randomness and modulo bias.

Modern Alternative

Instead of rand(), always prefer the <random> library introduced in C++11. Here’s why:

  • Better Random Engines: Engines like std::mt19937 offer higher quality randomness and larger ranges.
  • Distribution Support: Easily generate random numbers from uniform, normal, exponential, and other distributions.
  • Thread-Safe: By creating separate engines, you avoid global state issues.

⚠️ Warning: Avoid using rand() in new code. While it might still appear in older codebases, transitioning to the <random> library ensures better performance, safety, and flexibility.

Best Practices

Generating random numbers effectively requires following certain best practices to ensure correctness, performance, and security. Here are some guidelines to keep in mind when working with random numbers in C++:

1. Use std::random_device for Seeding

Always initialize your random number engine with a non-deterministic seed for unpredictable results. Avoid hardcoding seeds unless reproducibility is required for debugging or testing.

Example: Seeding a Random Engine
#include <random>

int main() {
    std::random_device rd;      // Non-deterministic seed
    std::mt19937 gen(rd());     // Mersenne Twister engine seeded with random_device

    // Use the engine for generating random numbers
    return 0;
}

2. Choose the Right Engine

Use the appropriate random number engine based on your requirements:

  • std::mt19937: High-quality pseudo-random numbers for general-purpose use.
  • std::default_random_engine: A simple option, though not recommended for critical tasks.
  • std::ranlux48: Suitable for scientific simulations requiring very high-quality randomness.

3. Use Distributions for Control

Always pair your random engine with a distribution to control the range and probability of generated numbers. Avoid manual range manipulation (e.g., % operator) to prevent bias.

Example: Using Distributions
#include <random>
#include <iostream>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dis(1, 10); // Uniform range: 1 to 10

    std::cout << "Random number: " << dis(gen) << std::endl;

    return 0;
}

4. Reuse Engines for Efficiency

Creating new engines repeatedly can be computationally expensive. Instead, create a single engine and reuse it throughout your program.

Example: Reusing a Random Engine
#include <random>
#include <iostream>

class RandomGenerator {
private:
    std::mt19937 gen;
    std::uniform_int_distribution<int> dis;

public:
    RandomGenerator(int min, int max)
        : gen(std::random_device{}()), dis(min, max) {}

    int generate() {
        return dis(gen);
    }
};

int main() {
    RandomGenerator dice(1, 6);

    for (int i = 0; i < 5; ++i) {
        std::cout << "Roll: " << dice.generate() << std::endl;
    }

    return 0;
}

5. Consider Thread Safety

Random engines and distributions are not thread-safe. In multi-threaded applications, ensure each thread uses its own random engine to avoid race conditions.

💡 Tip: Use thread-local storage or pass engine instances explicitly to threads for safe random number generation in parallel applications.

6. Avoid Legacy Functions

Avoid using rand() and srand() in new code. These functions have poor randomness quality, limited range, and lack support for distributions.

7. Cryptographic Random Numbers

If you need random numbers for cryptographic purposes, use specialized libraries like OpenSSL, libsodium, or std::random_device (if backed by a secure source).

Summary of Best Practices

  • Use std::random_device for seeding.
  • Choose the right engine for your use case.
  • Always use distributions to control output.
  • Reuse engines to improve efficiency.
  • Ensure thread safety in multi-threaded applications.
  • Avoid legacy random functions like rand().
  • Use secure libraries for cryptographic randomness.

💡 Key Takeaway: The <random> library in C++11+ provides everything you need for high-quality random number generation. Use the appropriate engine, distribution, and seeding method to ensure correctness and performance.

Conclusion

Modern C++ provides robust tools for generating random numbers through the <random> library. By using appropriate random engines like std::mt19937 combined with suitable distributions, you can generate high-quality random numbers for any application. Remember to avoid the legacy rand() function in new code, and always consider the statistical properties required for your specific use case. Whether you're developing games, running simulations, or performing statistical analysis, proper random number generation is crucial for reliable results.

Congratulations on reading to the end of this tutorial! To use our related calculators and for more documentation and articles, see the Further Reading section below.

Further Reading

Attribution and Citation

If you found this guide helpful, feel free to link back to this page or cite it in your work!

Profile Picture
Senior Advisor, Data Science | [email protected] |  + posts

Suf is a senior advisor in data science with deep expertise in Natural Language Processing, Complex Networks, and Anomaly Detection. Formerly a postdoctoral research fellow, he applied advanced physics techniques to tackle real-world, data-heavy industry challenges. Before that, he was a particle physicist at the ATLAS Experiment of the Large Hadron Collider. Now, he’s focused on bringing more fun and curiosity to the world of science and research online.

Buy Me a Coffee ✨