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
235: The <random> Header: Distributions and Engines
If you've spent any time in older C++ codebases, you've probably seen std::rand() and the modulo operator (%) used to constrain numbers. Honestly, stop doing that. std::rand() has poor statistical properties and the modulo approach introduces "modulo bias," making some numbers appear more often than others. Modern C++ gives us the <random> header, which separates the generation of raw random bits from the shaping of those bits into a useful range or pattern.
Picking the Engine
Think of the "engine" as the source of entropy. It's a stateful object that just spits out a sequence of raw, unbiased numbers. For almost every practical purpose, you want std::mt19937. It's a Mersenne Twister—fast, has a massive period before it repeats, and is far more reliable than the old C-style generators.
To get this started, we need a seed. We don't want the same "random" sequence every time the program runs, so we use std::random_device to grab a truly random seed from the hardware.
#include <iostream>
#include <random>
#include <string>
#include <vector>
int main() {
// The seed source
std::random_device rd;
// The engine, initialized with the seed
std::mt19937 gen(rd());
return 0;
}
Defining the Loot Table with Uniform Distributions
Now that we have a source of randomness, we need a "distribution." This is the part that takes the raw output of the engine and maps it to something we actually care about. Since we're building a simple game loot system, we'll start with a std::uniform_int_distribution to pick an item from a list.
std::vector<std::string> items = {"Rusty Sword", "Healing Potion", "Wooden Shield", "Iron Ore"};
// We want a random index between 0 and the last element of our vector
std::uniform_int_distribution<int> dist(0, items.size() - 1);
// To get a number, we pass the engine into the distribution object
int index = dist(gen);
std::cout << "You found a: " << items[index] << std::endl;
The "Same Number" Trap
Here is where I usually trip up when I'm rushing a prototype. I might decide to wrap the loot logic into a helper function to keep main() clean. I'll do something like this:
// DANGER: Don't do this
int rollLoot() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(0, 3);
return dist(gen);
}
If you call this function in a tight loop, you might notice the "random" numbers start looking very suspicious, or your performance tanks. Why? Because I'm re-seeding and re-instantiating the entire engine every single time the function is called. The engine is meant to be a long-lived object. You seed it once, and you keep using it.
The fix is simple: make the engine static so it's initialized only once for the lifetime of the program, or pass it by reference into the function. I prefer static for simple utility functions.
int rollLoot() {
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(0, 3);
return dist(gen);
}
Adding Variance with Normal Distributions
Uniform distributions are great for "one of these," but real-world data (and game balance) often follows a bell curve. Let's say each item has a "Quality Score." Most items should be average, a few should be terrible, and a few should be legendary. This is exactly what std::normal_distribution is for.
It takes a mean (the center of the bell) and a standard deviation (how spread out the curve is).
// Mean of 50, standard deviation of 10
std::normal_distribution<double> qualityDist(50.0, 10.0);
double quality = qualityDist(gen);
std::cout << "Item Quality: " << quality << std::endl;
Now, if we combine these, we have a system that can pick a random item and then assign it a realistically varied quality score. The engine generates the raw noise, the uniform_int_distribution picks the item, and the normal_distribution determines the quality.
📋 Practical Task
Build a Combat Critical-Hit Generator
Create a program that simulates 10 attacks of a character. For each attack, you need to implement the following logic using the <random> header:
- Critical Chance: Use a
std::bernoulli_distribution(which returns a boolean) with a probability of 0.15 (15%) to determine if the hit is a "Critical Hit." - Damage Calculation:
- If it is a normal hit, use a
std::uniform_int_distributionto generate damage between 10 and 20. - If it is a critical hit, use a
std::normal_distributionto generate damage with a mean of 40 and a standard deviation of 5.
- If it is a normal hit, use a
Ensure your std::mt19937 engine is initialized only once (hint: use static or declare it in main and pass it to a function) to avoid the re-seeding mistake discussed in the lesson. Print the result of each attack (e.g., "Normal Hit: 14 damage" or "CRITICAL HIT: 42.3 damage").
There are no comments for now.