<< Home

Chapter 25: Numerics, Randomness, and Time

C++ gives you precise control over numbers, random sampling, and time measurement. This chapter explains floating point behavior, fixed width integers and limits, robust random number facilities, and the modern <chrono> tools for clocks, durations, time points, and formatting.

Floating point behavior and pitfalls

Floating point types model real numbers with finite precision. Operations round to the nearest representable value, which means algebraic identities do not always hold. Correct code accepts small error, compares with tolerances, and uses the right functions for classification and rounding.

Representation and rounding

float, double, and long double store numbers as sign, exponent, and significand. Each operation rounds to fit that model. Small gaps between adjacent representable values get larger as magnitudes grow, so adding a tiny number to a huge number may do nothing.

double x = 1e16;
double y = 1.0;
double z = (x + y) - x;  // z may be 0.0 because y was lost to rounding

Equality and tolerances

Check closeness, not exact equality. Use absolute tolerances for small magnitudes and relative tolerances for large magnitudes. Blend both for general code.

bool nearly_equal(double a, double b, double rel=1e-12, double abs=1e-15) {
  double diff = std::fabs(a - b);
  return diff <= std::max(rel * std::max(std::fabs(a), std::fabs(b)), abs);
}
πŸ’‘ Prefer std::fma when available to compute a * b + c with a single rounding; this improves stability in many kernels.

Special values and classification

IEEE arithmetic supports infinities and NaNs. Use classification functions to branch safely and avoid undefined behavior in edge cases.

#include <cmath>
double q = 0.0, r = -0.0;
bool s1 = std::isnan(0.0/0.0);  // true
bool s2 = std::isinf(1.0/0.0);  // true
bool s3 = std::signbit(r);      // true for negative zero
⚠️ NaN never compares equal, not even to itself. Use std::isnan to detect it. Propagation rules vary by operation, so treat NaN as contagious.

Stable summation

Summing many values can accumulate error. A compensated algorithm reduces drift by tracking lost low order bits and feeding them back on the next step.

double kahan_sum(const std::vector<double>& v) {
  double sum = 0.0, c = 0.0;
  for (double x : v) {
    double y = x - c;
    double t = sum + y;
    c = (t - sum) - y;
    sum = t;
  }
  return sum;
}

Numeric limits and fixed width integers

When you need exact sizes and boundaries, use fixed width types and query their properties through std::numeric_limits. This keeps code portable and correct across platforms and compilers.

Choosing integer widths

Pick the smallest type that holds the full range of your data; this reduces memory traffic and keeps intent clear. Use signed types when negative values occur, otherwise prefer unsigned only for bit masks and modular arithmetic.

FamilyHeaderExamplesTypical use
Fixed width<cstdint>int8_t, uint32_t, int64_tBinary formats, hashes
Fast width<cstdint>int_fast16_t, …Speed on given platform
Least width<cstdint>int_least32_t, …Minimum bits with minimal size
Size types<cstddef>size_t, ptrdiff_tIndices and pointer diffs

Querying limits and properties

Use std::numeric_limits<T> to discover min, max, epsilon, and behavior flags. This avoids hardcoded constants and covers floating point details.

#include <limits>
auto maxu32 = std::numeric_limits<uint32_t>::max();
auto epsd  = std::numeric_limits<double>::epsilon();
bool is_ieee = std::numeric_limits<double>::is_iec559;  // IEEE 754?

Safe conversions and saturation

Converting between widths can overflow or truncate. Validate ranges before casting, or apply a saturating clamp for defensive code.

#include <algorithm>
uint8_t to_u8_saturate(int x) {
  return static_cast<uint8_t>(std::clamp(x, 0, 255));
}

Random number engines and distributions

The <random> library separates engines that produce raw bits from distributions that shape values. You seed an engine, then feed it through a distribution that models the desired behavior.

Engines and seeding

std::mt19937 is a common workhorse. Seed once from std::random_device or from a fixed seed for reproducible experiments. Keep engines in objects or thread local storage to avoid global contention.

#include <random>
std::mt19937 make_rng() {
  std::random_device rd;
  std::seed_seq seq{rd(), rd(), rd(), rd()};
  return std::mt19937{seq};
}

Uniform, normal, and discrete choices

Apply distributions to map engine output into meaningful values. Uniform distributions sample ranges, normal distributions model bell curves, and discrete distributions pick from weighted buckets.

auto rng = make_rng();
std::uniform_int_distribution<int>     die(1, 6);
std::uniform_real_distribution<double> unit(0.0, 1.0);
std::normal_distribution<double>       noise(0.0, 1.0);
std::discrete_distribution<int>        pick({1, 3, 1});  // weights
int d = die(rng);
double x = unit(rng);
double n = noise(rng);
int ix = pick(rng);
πŸ’‘ Prefer one engine per thread. Use thread_local std::mt19937 rng or pass engines down the call chain to keep results predictable and fast.

Reproducibility and serialization

Deterministic runs are vital for tests. Save and restore engine state; then rerun with the same sequence later.

std::mt19937 rng = make_rng();
std::string state;
{
  std::ostringstream os;
  os << rng;
  state = os.str();
}
int a = std::uniform_int_distribution<int>(0, 9)(rng);
// restore
{
  std::istringstream is(state);
  is >> rng;
}
int b = std::uniform_int_distribution<int>(0, 9)(rng); // matches prior path after restore

chrono clocks, durations, and time points

<chrono> measures time with strong types. A clock tells the current time, a duration represents a length of time, and a time_point binds a clock to a specific instant.

Clocks you will use

system_clock tracks civil time and can jump when the system time is adjusted. steady_clock is monotonic and does not jump, which makes it ideal for measuring intervals. high_resolution_clock is an alias to the highest resolution clock available on your platform.

#include <chrono>
using namespace std::chrono;
auto t0 = steady_clock::now();
// work …
auto ms = duration_cast<milliseconds>(steady_clock::now() - t0);

Durations and literals

Durations carry both representation and period. Use suffixes to write readable code. Convert with duration_cast when narrowing or when you need a specific unit.

using namespace std::chrono_literals;
auto wait = 150ms + 2s;
auto us = std::chrono::duration_cast<std::chrono::microseconds>(wait);

Time points and arithmetic

Add durations to time points, or subtract time points to get durations. Convert between clocks only through approved utilities such as clock_cast when available.

auto start = std::chrono::steady_clock::now();
// …
auto elapsed = std::chrono::steady_clock::now() - start; // duration
⚠️ Use steady_clock for benchmarking. system_clock can move backwards or forwards if the system time is adjusted, which breaks elapsed time math.

Date and time formatting

C++20 adds std::format support for time types plus full date formatting in <chrono>. You can print durations and time points using familiar format strings, and you can work with time zones through zoned_time and the IANA database when your standard library provides it.

Formatting durations

Formatters understand durations directly. Choose units with a cast before formatting to control the output.

#include <format>
using namespace std::chrono_literals;
auto d = 12345ms;
auto s = std::format("{} ms", std::chrono::duration_cast<std::chrono::milliseconds>(d).count());

Formatting time points

Convert a time point to calendar form with floor or use std::chrono::format with specifiers that mirror strftime patterns. The braces carry the pattern; options go inside as {…}.

#include <chrono>
#include <format>
using namespace std::chrono;
auto now = system_clock::now();
std::string iso = std::chrono::format("{:%Y-%m-%dT%H:%M:%S%Ez}", now);

Time zones and zoned_time

Bind a clock time to a named zone for correct civil time handling, including daylight saving rules. This requires a time zone database at runtime, which many standard libraries ship.

#include <chrono>
using namespace std::chrono;
zoned_time zt{"Europe/London", system_clock::now()};
auto local = std::chrono::format("{:%F %T %Z}", zt);  // prints date, time, and zone

Parsing dates from strings

Use std::chrono::parse to turn text into time points or durations. Match the pattern to your input exactly to avoid failure.

#include <chrono>
#include <sstream>
using namespace std::chrono;
std::istringstream is{"2025-11-06 20:00"};
sys_seconds tp;
is >> parse("%F %R", tp);  // tp holds a UTC time_point at minute resolution