C++ std::random_device | Hardware entropy for seeding
Key takeaways
How random_device maps to OS entropy, using entropy(), seed_seq for mt19937, UUID and token examples, performance vs mt19937, and platform quirks when entropy() is zero.
Introduction
std::random_device is a hardware-based random number generator introduced in C++11. Generates non-deterministic random numbers using the system’s entropy source (e.g. /dev/urandom), primarily used to seed random number engines or for cryptographic purposes.
The reason random_device exists as a separate type from mt19937 and the other engines covered in the companion random guide is that it solves a fundamentally different problem: those engines are fast, deterministic algorithms that produce a long, statistically well-distributed sequence from a starting seed — but a deterministic algorithm can never produce output an attacker couldn’t reproduce if they knew the seed. random_device is the library’s answer to “where does that initial seed actually come from,” by tapping into the operating system’s own entropy pool, which draws from genuinely unpredictable physical sources — timing jitter, hardware interrupts, thermal noise, depending on the platform.
random_device default
Default Enabled
#include <iostream>
#include <random>
int main() {
std::random_device rd;
// Generate random numbers (32-bit unsigned int)
for (int i = 0; i < 5; ++i) {
std::cout << rd() << std::endl;
}
return 0;
}
Example output:
3847291038
1029384756
2938475610
4756102938
1847562903
random_device implements the same callable interface as mt19937 and every other engine — calling rd() returns one unsigned int per call, which is what makes it possible to use random_device directly wherever an engine is expected. The catch, covered in detail in the performance section below, is that it usually shouldn’t be — each call typically makes a system call into the OS’s entropy source, which is orders of magnitude slower than an in-memory PRNG’s arithmetic.
Seed generation
#include <random>
#include <iostream>
int main() {
// Generate a seed from random_device
std::random_device rd;
// Initialize the mt19937 engine
std::mt19937 gen{rd()};
// Uniform distribution
std::uniform_int_distribution<> dist{1, 100};
// Generate random numbers (fast)
for (int i = 0; i < 10; ++i) {
std::cout << dist(gen) << " ";
}
std::cout << std::endl;
return 0;
}
Example output:
42 17 89 3 56 91 28 64 11 73
This is the pattern the rest of this guide builds on: call rd() exactly once to seed gen, then draw as many numbers as needed from gen (via a distribution) instead of calling rd() again. random_device pays its entropy cost a single time; every subsequent random number comes from the cheap, deterministic mt19937 state machine that cost was used to initialize.
Entropy
entropy() method
#include <iostream>
#include <random>
int main() {
std::random_device rd;
// Check entropy (bits)
double entropy = rd.entropy();
std::cout << "Entropy: " << entropy << " bits" << std::endl;
if (entropy == 0.0) {
std::cout << "Pseudorandom (deterministic)" << std::endl;
} else {
std::cout << "Hardware random (non-deterministic)" << std::endl;
}
return 0;
}
Output (Linux):
Entropy: 32 bits
Hardware random (non-deterministic)
entropy() is meant to report how much genuine randomness backs each generated value, but the standard deliberately leaves its exact meaning implementation-defined — this is the one place random_device’s API admits it can’t make a hard promise. In practice, treat entropy() == 0.0 as an explicit signal, not an edge case to ignore: several standard library implementations (and virtually all embedded targets without a hardware RNG) legitimately return 0 to indicate random_device has silently fallen back to a deterministic pseudo-random source, meaning the “hardware randomness” you were counting on for a cryptographic key or token isn’t actually there.
Platform-specific implementation
| platform | entropy source | entropy() |
|---|---|---|
| Linux | /dev/urandom | 32 bits |
| Windows | CryptGenRandom | 32 bits |
| macOS | /dev/urandom | 32 bits |
| Some Embedded | pseudorandom number | 0 bits |
This table is exactly why checking entropy() before relying on random_device for anything security-sensitive is a real, practical step rather than defensive paranoia — code developed and tested on a Linux or macOS workstation, where entropy() reliably reports 32, can behave completely differently once deployed to embedded hardware with no hardware entropy source at all.
Seed sequence
Single seed vs multiple seeds
#include <random>
#include <array>
#include <algorithm>
int main() {
std::random_device rd;
// ❌ Single seed (only 32 bits used)
std::mt19937 gen1{rd()};
// ✅ Multiple seeds (better initialization)
std::array<unsigned int, std::mt19937::state_size> seedData;
std::generate(seedData.begin(), seedData.end(), std::ref(rd));
std::seed_seq seq(seedData.begin(), seedData.end());
std::mt19937 gen2{seq};
return 0;
}
Description:
- The state size of
mt19937is 624 32-bit integers (19,968 bits). - A single seed uses only 32 bits (the rest is filled with the algorithm)
- Multiple seeds provide more entropy
std::generate(seedData.begin(), seedData.end(), std::ref(rd)) is worth reading carefully — std::generate calls its generator argument once per element and stores the result, so this line calls rd() 624 separate times, once per word of mt19937’s internal state, rather than the single call used for gen1. std::ref(rd) matters here specifically because random_device is non-copyable (copying a live entropy source doesn’t make sense), so passing it by value to std::generate wouldn’t compile — std::ref wraps it in a copyable reference wrapper instead. The practical payoff: gen1’s entire future output sequence is fully determined by 32 bits of real entropy, meaning only 2³² possible starting states exist for an attacker to potentially search; gen2’s state is seeded from 624 independent 32-bit values, closing that gap.
Practical example
Example 1: Cryptographically random numbers
#include <random>
#include <vector>
#include <iostream>
#include <iomanip>
std::vector<unsigned char> generateKey(size_t length) {
std::random_device rd;
std::vector<unsigned char> key(length);
for (auto& byte : key) {
byte = static_cast<unsigned char>(rd() % 256);
}
return key;
}
int main() {
// Generate a 16-byte key
auto key = generateKey(16);
std::cout << "Key: ";
for (auto byte : key) {
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(byte);
}
std::cout << std::endl;
return 0;
}
Example output:
Key: 3f7a9b2c8d1e4f6a5b3c9d2e7f1a4b8c
Despite the section heading, treat this specific pattern as illustrative, not as production-ready cryptographic code — rd() % 256 throws away most of each 32-bit call to extract a single byte, which is wasteful, and more importantly, the C++ standard explicitly does not guarantee random_device is cryptographically secure on every platform (that guarantee is implementation-defined, tying back to the entropy() caveat above). For real cryptographic key generation, use your platform’s dedicated CSPRNG API (getrandom() on Linux, BCryptGenRandom on Windows) or a vetted library like OpenSSL/libsodium — random_device is the right tool for seeding a mt19937 used for simulations or games, not for generating keys that need to resist a determined attacker.
Example 2: UUID generation
#include <random>
#include <sstream>
#include <iomanip>
#include <iostream>
std::string generateUUID() {
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<> dist{0, 15};
std::uniform_int_distribution<> dist2{8, 11};
std::ostringstream oss;
oss << std::hex;
// 8-4-4-4-12 format
for (int i = 0; i < 8; ++i) oss << dist(gen);
oss << "-";
for (int i = 0; i < 4; ++i) oss << dist(gen);
oss << "-4"; // version 4
for (int i = 0; i < 3; ++i) oss << dist(gen);
oss << "-";
oss << dist2(gen); // variant (8, 9, a, b)
for (int i = 0; i < 3; ++i) oss << dist(gen);
oss << "-";
for (int i = 0; i < 12; ++i) oss << dist(gen);
return oss.str();
}
int main() {
for (int i = 0; i < 3; ++i) {
std::cout << generateUUID() << std::endl;
}
return 0;
}
Example output:
550e8400-e29b-41d4-a716-446655440000
6ba7b810-9dad-11d1-80b4-00c04fd430c8
3d813cbb-47fb-32ba-91df-831e1593ac29
The hardcoded "-4" and the separate dist2{8, 11} aren’t arbitrary formatting choices — they’re what makes this a valid UUID version 4 (random) rather than just a random-looking string. The UUID spec reserves specific bit positions to encode the UUID’s version and variant, so a real v4 UUID always has a literal 4 in that position and one of 8/9/a/b in the next group — encoding that fixed structure is what lets other software correctly recognize the string as a v4 UUID rather than some other version.
Example 3: Token generation
#include <random>
#include <string>
#include <iostream>
std::string generateToken(size_t length) {
std::random_device rd;
std::mt19937 gen{rd()};
const std::string chars =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
std::uniform_int_distribution<> dist{0, static_cast<int>(chars.size() - 1)};
std::string token;
token.reserve(length);
for (size_t i = 0; i < length; ++i) {
token += chars[dist(gen)];
}
return token;
}
int main() {
// Generate a 32-character token
std::cout << "Token: " << generateToken(32) << std::endl;
return 0;
}
Example output:
Token: 7aB3xK9mP2qW5nL8vC1dF4jH6rT0yU3z
token.reserve(length) before the loop is a small but real optimization worth noticing — without it, std::string’s internal buffer grows by repeated reallocation as characters are appended one at a time, whereas reserve allocates the final buffer size up front so the loop below never triggers a reallocation at all.
Frequently occurring problems
Issue 1: Performance
#include <random>
#include <chrono>
#include <iostream>
void benchmarkRandomDevice() {
std::random_device rd;
auto start = std::chrono::high_resolution_clock::now();
// ❌ Using random_device directly (very slow)
for (int i = 0; i < 1000000; ++i) {
volatile unsigned int r = rd();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "random_device: " << duration.count() << "ms" << std::endl;
}
void benchmarkMT19937() {
std::random_device rd;
std::mt19937 gen{rd()};
auto start = std::chrono::high_resolution_clock::now();
// ✅ Using mt19937 (fast)
for (int i = 0; i < 1000000; ++i) {
volatile unsigned int r = gen();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "mt19937: " << duration.count() << "ms" << std::endl;
}
int main() {
benchmarkRandomDevice(); // ~5000ms
benchmarkMT19937(); // ~50ms
return 0;
}
Example output:
random_device: 5234ms
mt19937: 47ms
The roughly 100x gap between these two numbers is the single most important practical takeaway in this guide: it’s the difference between a system call crossing into the kernel to read from an entropy pool (random_device) and a handful of in-memory arithmetic operations (mt19937). volatile on r in both loops exists to stop the compiler from noticing the value is never used and optimizing the entire loop away — without it, an aggressive compiler could legally eliminate both benchmarks down to nothing, since neither generated value has any observable effect on the program.
Issue 2: Platform Dependency
#include <random>
#include <iostream>
int main() {
std::random_device rd;
// Check entropy
double entropy = rd.entropy();
if (entropy == 0.0) {
std::cerr << "Warning: random_device is using a pseudorandom fallback." << std::endl;
std::cerr << "Platform: hardware randomness not supported" << std::endl;
// Fallback: time-based seed
auto now = std::chrono::high_resolution_clock::now();
auto seed = now.time_since_epoch().count();
std::mt19937 gen{static_cast<unsigned int>(seed)};
} else {
std::cout << "Using hardware randomness (entropy: " << entropy << " bits)" << std::endl;
}
return 0;
}
Falling back to a time-based seed when entropy() reports zero is a reasonable default for non-critical randomness (game logic, simulations), but it’s worth being honest about what it actually buys you: a time-based seed is predictable to anyone who has a rough idea of when your program started, which is exactly the property random_device exists to avoid. If entropy() reporting zero matters for your use case — because you actually need unpredictability, not just variety — the right response is usually to fail loudly or fall back to a platform CSPRNG API, not to silently downgrade to a weaker seed and hope nobody notices.
Issue 3: Seed quality
#include <random>
#include <array>
#include <algorithm>
// ❌ Single seed (low quality)
void poorSeeding() {
std::random_device rd;
std::mt19937 gen{rd()}; // only 32 bits used
}
// ✅ Multiple seeds (high quality)
void goodSeeding() {
std::random_device rd;
// Generate as many seed words as mt19937's state size
std::array<unsigned int, std::mt19937::state_size> seedData;
std::generate(seedData.begin(), seedData.end(), std::ref(rd));
std::seed_seq seq(seedData.begin(), seedData.end());
std::mt19937 gen{seq};
}
// ✅ Simple multiple-seed version
void simpleGoodSeeding() {
std::random_device rd;
// 8 seed words (256 bits)
std::array<unsigned int, 8> seedData;
for (auto& seed : seedData) {
seed = rd();
}
std::seed_seq seq(seedData.begin(), seedData.end());
std::mt19937 gen{seq};
}
simpleGoodSeeding is the practical middle ground most real code should reach for: seeding with the full 624-word state size (goodSeeding) is the theoretically most thorough option, but 8 words (256 bits) of entropy is already far more than enough to make the seed space unsearchable for any realistic attacker, at a fraction of the random_device calls — each of which, per Issue 1, is comparatively expensive.
Issue 4: Reproducibility
#include <random>
#include <iostream>
// ❌ Not reproducible (hard to debug)
void nonReproducible() {
std::random_device rd;
std::mt19937 gen{rd()}; // a different seed every time
std::uniform_int_distribution<> dist{1, 100};
std::cout << dist(gen) << std::endl; // a different result every time
}
// ✅ Reproducible (easy to debug)
void reproducible(bool debug = false) {
std::mt19937 gen;
if (debug) {
gen.seed(42); // fixed seed
} else {
std::random_device rd;
gen.seed(rd()); // random seed
}
std::uniform_int_distribution<> dist{1, 100};
std::cout << dist(gen) << std::endl;
}
int main() {
std::cout << "Debug mode:" << std::endl;
reproducible(true); // always the same result
reproducible(true); // always the same result
std::cout << "\nProduction mode:" << std::endl;
reproducible(false); // a different result every time
reproducible(false); // a different result every time
return 0;
}
This debug flag is worth adopting as a general pattern for any code that depends on randomness — a test suite that calls nonReproducible()-shaped code can never reliably reproduce a failure, since every run draws a fresh, unrecoverable seed from random_device. Threading a debug/fixed-seed path through your random-number-consuming code (as reproducible does here) means a failing test can be pinned to an exact, replayable sequence, while production code still gets genuine unpredictability from random_device.
Practical example: random number utility
#include <random>
#include <string>
#include <vector>
#include <array>
#include <algorithm>
class RandomUtils {
std::mt19937 gen;
public:
// Constructor: high-quality seed
RandomUtils() {
std::random_device rd;
std::array<unsigned int, 8> seedData;
std::generate(seedData.begin(), seedData.end(), std::ref(rd));
std::seed_seq seq(seedData.begin(), seedData.end());
gen.seed(seq);
}
// Integer within a range
int randomInt(int min, int max) {
std::uniform_int_distribution<> dist{min, max};
return dist(gen);
}
// Real number within a range
double randomDouble(double min, double max) {
std::uniform_real_distribution<> dist{min, max};
return dist(gen);
}
// Random string
std::string randomString(size_t length) {
const std::string chars =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
std::uniform_int_distribution<> dist{0, static_cast<int>(chars.size() - 1)};
std::string result;
result.reserve(length);
for (size_t i = 0; i < length; ++i) {
result += chars[dist(gen)];
}
return result;
}
// Random bytes
std::vector<unsigned char> randomBytes(size_t length) {
std::uniform_int_distribution<> dist{0, 255};
std::vector<unsigned char> bytes(length);
for (auto& byte : bytes) {
byte = static_cast<unsigned char>(dist(gen));
}
return bytes;
}
// Shuffle an array
template<typename T>
void shuffle(std::vector<T>& vec) {
std::shuffle(vec.begin(), vec.end(), gen);
}
};
int main() {
RandomUtils rng;
// Integer
std::cout << "Random int (1-100): " << rng.randomInt(1, 100) << std::endl;
// Real number
std::cout << "Random double (0-1): " << rng.randomDouble(0.0, 1.0) << std::endl;
// String
std::cout << "Random string: " << rng.randomString(16) << std::endl;
// Bytes
auto bytes = rng.randomBytes(8);
std::cout << "Random bytes: ";
for (auto byte : bytes) {
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(byte) << " ";
}
std::cout << std::endl;
// Shuffle
std::vector<int> vec = {1, 2, 3, 4, 5};
rng.shuffle(vec);
std::cout << "Shuffled: ";
for (int x : vec) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
Example output:
Random int (1-100): 73
Random double (0-1): 0.642857
Random string: aB7xK3mP9qW2nL5v
Random bytes: 3f 7a 9b 2c 8d 1e 4f 6a
Shuffled: 3 1 5 2 4
RandomUtils ties together every idea in this guide into one reusable class: the constructor pays the random_device entropy cost exactly once, using the multi-word seeding pattern from Issue 3, and every method afterward draws from the cheap, already-seeded gen — the same “seed once, reuse many times” shape as the very first example, just packaged as a class instead of loose functions. This is a reasonable template to copy directly into a project that needs random values in more than one place, rather than re-deriving the seeding boilerplate at every call site.
Cleanup
Key takeaways
- random_device: Hardware-based non-deterministic random numbers
- Use: Seed generation, cryptographic random numbers
- Performance: Slow (only used as seed)
- entropy(): non-determinism measure (pseudo-random if 0)
- Seed Quality: Initialize with multiple seeds
- Reproducibility: Use fixed seeds when debugging
random_device vs mt19937
| Features | random_device | mt19937 |
|---|---|---|
| Speed | very slow | Fast |
| Quality | hardware random numbers | pseudorandom number |
| Use | seed generation | general random number |
| Reproducibility | Not possible | Possible (seed fixed) |
| Platform | Dependent | Independent |
Practical tips
Principle of use:
random_deviceis only used for seed generation- Actual random numbers use engines such as
mt19937 - For genuinely cryptographic purposes, prefer a platform CSPRNG API over
random_devicealone (see Example 1) - Initialize engine with multiple seeds (improves quality) Performance:
random_deviceuses system call (slow)- Engine is memory based (fast)
- For large random numbers, an engine must be used. Debugging:
- Use fixed seed when debugging
random_deviceseed in production- Verify platform by checking
entropy()
Related Articles
- C++ Distribution | Probability Distribution Guide
- C++ random
- C++ Random Number Generation | ‘random’ Library Guide
- C++ Algorithm Generate Guide
- C++ Algorithm Reverse | reverse, rotate, reverse_copy