C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
202: Random Number Generation with <random>
I've seen this exact snippet of code in countless junior PRs and student projects. It looks innocent enough, but it's a disaster in production. Let's look at a function designed to simulate a critical hit in a game:
#include <iostream>
#include <cstdlib>
#include <ctime>
bool isCriticalHit() {
std::srand(std::time(nullptr)); // Seed the generator
return (std::rand() % 100) < 10; // 10% chance
}
int main() {
for (int i = 0; i < 5; ++i) {
std::cout << (isCriticalHit() ? "CRIT! " : "Miss ") <;
}
return 0;
}
If you run this, you'll notice something weird. Instead of a random sequence, you'll likely get the exact same result five times in a row (e.g., "Miss Miss Miss Miss Miss"). It feels broken because it is broken.
The problem with seeding inside a loop
The issue here is that std::time(nullptr) returns the current time in seconds. Because modern CPUs execute that loop in a tiny fraction of a second, std::srand() is being called with the exact same seed value every single time the function runs. In the world of pseudo-random number generation, if you provide the same seed, you get the exact same sequence of numbers. You're essentially resetting the "random" sequence back to the start every time you check for a crit.
Even if you moved the seed to main(), std::rand() is generally frowned upon in modern C++. It often has poor distribution (the lower bits are less random than the higher bits) and a very small maximum range (RAND_MAX can be as low as 32,767 on some platforms), which makes it useless for high-precision simulations.
Switching to the Mersenne Twister
To do this properly, we need the <random> header. Instead of a global state that we seed with srand, we create a generator object. The industry standard for most general-purpose work is std::mt19937, which is a Mersenne Twister engine. It has a massive period, meaning it takes a very long time before the sequence repeats.
Here is how I would rewrite that critical hit logic:
#include <iostream>
#include <random>
bool isCriticalHit() {
// 'static' ensures the engine is initialized only once for the life of the program
static std::random_device rd;
static std::mt19937 gen(rd());
// We define the range here: 0 to 99
std::uniform_int_distribution<int> dist(0, 99);
return dist(gen) < 10;
}
Notice the static keyword. This is crucial. I don't want to re-seed the engine every time the function is called (which brings us back to the original bug), but I also don't want to pass a generator object through every single function call in my game engine. By making it static, the engine lives for the duration of the program, and we just sample from it.
Decoupling Generation from Distribution
One of the biggest shifts in <random> compared to the old rand() % N approach is the separation of the engine and the distribution. The engine (mt19937) just spits out a raw stream of huge, random numbers. The distribution (uniform_int_distribution) takes that raw stream and shapes it into the range and type you actually need.
Why bother with this complexity? Because rand() % 100 suffers from "modulo bias." If RAND_MAX isn't perfectly divisible by 100, some numbers will appear slightly more often than others. While that doesn't matter for a casual game, it's a nightmare for cryptography or scientific simulations. Using a distribution object handles the math for you, ensuring every number in your range has an mathematically equal chance of appearing.
Handling Floating Point Ranges
If you need a random decimal—say, for a damage multiplier between 1.5x and 2.5x—you don't do the weird casting gymnastics required by rand(). You just swap the distribution type:
std::uniform_real_distribution<double> damageDist(1.5, 2.5);
double multiplier = damageDist(gen);
It's clean, explicit, and doesn't rely on some hidden global state that might be modified by another library you've included. Just remember: seed your engine once, and use distributions to shape the output.
📋 Practical Task
Exercise: Implementing a Weighted Loot Table
In many RPGs, loot isn't perfectly uniform. A "Common" item might have a 70% drop rate, "Rare" 20%, and "Legendary" 10%.
Your task is to create a program that simulates a loot drop. Use the <random> library to implement the following logic:
- Create a
std::mt19937generator seeded withstd::random_device. - Use a
std::uniform_int_distribution<int>to generate a number between 1 and 100. - Based on the result, print the rarity of the item:
- 1-70: "Common Item"
- 71-90: "Rare Item"
- 91-100: "Legendary Item!"
- Wrap this in a loop that simulates 10 separate loot drops to verify the distribution feels correct.
Challenge: Try making the generator static inside a function called std::string rollLoot() so that the function can be called multiple times without re-seeding.
There are no comments for now.