Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Creating a Global Random Number Generator
Create a reusable random number generator accessible throughout your program.
Global Random Numbers (Random.h)
When building programs that need random numbers across multiple functions or source files, passing a PRNG object everywhere becomes cumbersome. While you could create separate static local PRNGs in each function, this leads to code duplication and potentially lower quality randomness due to insufficient use.
The ideal solution is a single, globally accessible random number generator. Although we generally discourage non-const global variables, random number generators are a reasonable exception to this rule.
A Header-Only Random Number Solution
Below is a complete, header-only implementation that provides a self-seeding Mersenne Twister accessible from any code file. This requires C++17 or newer.
Random.h:
#pragma once
#include <chrono>
#include <random>
// This header provides a self-seeding Mersenne Twister for easy random number generation.
// Requires C++17 or newer.
// Can be included in multiple translation units (inline prevents ODR violations)
namespace Random
{
// Helper function that returns a seeded Mersenne Twister
inline std::mt19937 create()
{
std::random_device rd{};
// Seed using both clock and multiple random_device calls
std::seed_seq seedSequence{
static_cast<std::seed_seq::result_type>(std::chrono::steady_clock::now().time_since_epoch().count()),
rd(), rd(), rd(), rd(), rd(), rd(), rd()
};
return std::mt19937{ seedSequence };
}
// Global Mersenne Twister instance (inline ensures single instance across translation units)
inline std::mt19937 generator{ create() };
// Generate random integer between [minimum, maximum] inclusive
// Handles mixed integer types automatically
inline int get(int minimum, int maximum)
{
return std::uniform_int_distribution{minimum, maximum}(generator);
}
// Template versions for other integer types (advanced - can be ignored initially)
// Generate random value between [minimum, maximum] when both have same type
// Supports: short, int, long, long long (signed and unsigned variants)
// Example: Random::get(1L, 100L); returns long
// Example: Random::get(1u, 100u); returns unsigned int
template <typename T>
T get(T minimum, T maximum)
{
return std::uniform_int_distribution<T>{minimum, maximum}(generator);
}
// Generate random value with explicit return type
// Useful when minimum and maximum have different types
// Example: Random::get<std::size_t>(0, 100); returns std::size_t
// Example: Random::get<long>(1, 100u); returns long
template <typename ReturnType, typename S, typename T>
ReturnType get(S minimum, T maximum)
{
return get<ReturnType>(static_cast<ReturnType>(minimum), static_cast<ReturnType>(maximum));
}
}
Using Random.h
Using this header is straightforward:
- Save the code above in a file named
Random.hin your project directory - Include
Random.hin any source file needing random numbers - Call
Random::get(minimum, maximum)whenever you need a random number
No manual initialization required - the generator seeds itself automatically when first accessed.
Example Usage
Here's a program demonstrating various ways to use Random.h:
main.cpp:
#include "Random.h"
#include <iostream>
#include <cstddef>
int main()
{
// Generate random numbers with matching argument types
std::cout << Random::get(1, 100) << '\n'; // int between 1-100
std::cout << Random::get(1u, 100u) << '\n'; // unsigned int between 1-100
// Generate with explicit return type when argument types differ
std::cout << Random::get<std::size_t>(0, 100u) << '\n'; // std::size_t between 0-100
// Direct access to generator for custom distributions
std::uniform_int_distribution scoreRange{ 0, 100 };
for (int student{ 1 }; student <= 10; ++student)
{
std::cout << scoreRange(Random::generator) << '\t';
}
std::cout << '\n';
return 0;
}
Example: Simulating Coin Flips
#include "Random.h"
#include <iostream>
int main()
{
constexpr int totalFlips{ 50 };
int headsCount{ 0 };
std::cout << "Flipping coin " << totalFlips << " times:\n";
for (int flip{ 1 }; flip <= totalFlips; ++flip)
{
bool isHeads{ Random::get(0, 1) == 1 };
std::cout << (isHeads ? 'H' : 'T') << ' ';
if (isHeads)
++headsCount;
if (flip % 10 == 0)
std::cout << '\n';
}
std::cout << "\nHeads: " << headsCount << ", Tails: " << (totalFlips - headsCount) << '\n';
return 0;
}
Example: Random Enemy Spawn
#include "Random.h"
#include <iostream>
#include <string_view>
std::string_view getRandomEnemy()
{
int enemyType{ Random::get(1, 5) };
switch (enemyType)
{
case 1: return "Goblin";
case 2: return "Orc";
case 3: return "Skeleton";
case 4: return "Zombie";
case 5: return "Dragon";
default: return "Unknown"; // Should never happen
}
}
int main()
{
std::cout << "Today's enemy encounters:\n";
for (int encounter{ 1 }; encounter <= 8; ++encounter)
{
std::cout << encounter << ". " << getRandomEnemy() << '\n';
}
return 0;
}
Implementation Details
The Random.h implementation uses several advanced C++ features:
Inline Variables and Functions
Normally, defining variables and functions in headers causes one-definition rule (ODR) violations when included in multiple source files. The inline keyword allows identical definitions across translation units without violating ODR.
When you include Random.h in multiple files, the compiler ensures only one Random::generator instance exists across your entire program.
Self-Seeding Pattern
We want Random::generator to automatically seed itself without requiring manual initialization. Since variable initializers must be expressions (not statements), we can't directly create helper objects like std::random_device and std::seed_seq.
The solution: a helper function! The create() function contains all necessary statements to create and return a fully-seeded generator. We then use this return value as the initializer for Random::generator.
This pattern ensures the generator seeds itself correctly the first time it's accessed, with no manual setup required from the programmer.
When to Access Random::generator Directly
While Random::get() handles most cases, you can access Random::generator directly when:
- Using custom distributions (like
std::normal_distributionfor bell curves) - Needing distributions with state (creating them once and reusing them)
- Building your own specialized random number utilities
Example with custom distribution:
#include "Random.h"
#include <iostream>
int main()
{
// Simulate test scores with normal distribution (mean=75, stddev=10)
std::normal_distribution<double> testScores{ 75.0, 10.0 };
std::cout << "Student test scores:\n";
for (int student{ 1 }; student <= 20; ++student)
{
double score{ testScores(Random::generator) };
// Clamp to valid range
if (score < 0.0) score = 0.0;
if (score > 100.0) score = 100.0;
std::cout << "Student " << student << ": "
<< static_cast<int>(score) << '\n';
}
return 0;
}
This header-only approach provides convenient random number generation without sacrificing flexibility or code organization.
Summary
Global random number generators provide convenient access to random numbers across multiple files without passing PRNG objects everywhere or duplicating initialization code.
Header-only implementation using inline variables and functions allows including Random.h in multiple translation units without violating the one-definition rule, ensuring a single shared generator instance.
Self-seeding pattern uses a helper function to perform initialization that cannot be done directly in a variable initializer, automatically seeding the generator when first accessed.
Random::get() function provides a simple interface for generating random integers in a specified range, handling distribution creation automatically.
Template versions support various integer types and allow explicit return type specification when argument types differ, providing flexibility for different use cases.
Direct generator access via Random::generator enables custom distributions like normal distributions for more specialized random number needs.
Usage simplicity requires only including the header and calling Random::get()—no manual initialization, seeding, or cleanup needed.
Combined seeding approach uses both clock values and multiple std::random_device calls to provide better seed quality than either approach alone.
This global random number solution balances convenience with good practices, providing a reusable component that simplifies random number generation across your codebase while maintaining quality and flexibility.
Creating a Global Random Number Generator - Quiz
Test your understanding of the lesson.
Practice Exercises
Random Number Generator Module
Create a reusable random number generator module using the header pattern.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!