There are so many kinds of numbers we deal with on a regular basis and the C++ Standard Library has a full suite of tools to deal with them. Today we’ll look into random numbers, ratios, mathematical constants, bit manipulation, complex numbers, and more!

Table of Contents

Limits

C# primitive type structs have const fields indicating their range: int.MinValue and int.MaxValue. Likewise, the C++ Standard Library’s <limits> header provides the std::numeric_limits class template. At its core, this provides a type-safe version of the macros in the C Standard Library’s <limits.h>/<climits> and <stdint.h>/<cstdint>:

#include <limits>
 
DebugLog(std::numeric_limits<int32_t>::min()); // -2147483648
DebugLog(std::numeric_limits<int32_t>::max()); // 2147483647

The min and max member functions are constexpr, so they can be used in compile-time programming just like the equivalent C# const fields.

There are a ton more functions and constants available in numeric_limits. Here’s a selection of them:

#include <limits>
 
// Difference between 1.0 and the next representable floating-point value
DebugLog(std::numeric_limits<float>::epsilon()); // 1.19209e-07
 
// Largest error in rounding a floating-point value
DebugLog(std::numeric_limits<float>::round_error()); // 0.5
 
// Floating-point constants
DebugLog(std::numeric_limits<float>::infinity()); // inf
DebugLog(std::numeric_limits<float>::quiet_NaN()); // nan
DebugLog(std::numeric_limits<float>::signaling_NaN()); // nan
 
// Type info useful when writing templates
DebugLog(std::numeric_limits<float>::is_integer); // false
DebugLog(std::numeric_limits<float>::is_exact); // false
DebugLog(std::numeric_limits<float>::is_modulo); // false
DebugLog(std::numeric_limits<float>::digits10); // 6
Numbers

The <numbers> header was introduced in C++20 to provide mathematical constants in the std::numbers namespace. C# has a few of these as const fields of Math, but the selection is limited and only double values are provided. C++ provides a more robust set as variable templates for each numeric type:

#include <numbers>
 
// Base 2 log of e
DebugLog(std::numbers::log2e_v<float>); // 1.4427
 
// Base 10 log of e
DebugLog(std::numbers::log10e_v<float>); // 0.434294
 
// Pi
DebugLog(std::numbers::pi_v<float>); // 3.14159
 
// 1 divided by pi
DebugLog(std::numbers::inv_pi_v<float>); // 0.31831
 
// 1 divided by the square root of pi
DebugLog(std::numbers::inv_sqrtpi_v<float>); // 0.56419
 
// Natural logarithm of 2
DebugLog(std::numbers::ln2_v<float>); // 0.693147
 
// Natural logarithm of 10
DebugLog(std::numbers::ln10_v<float>); // 2.30259
 
// Square root of 2
DebugLog(std::numbers::sqrt2_v<float>); // 1.41421
 
// Square root of 3
DebugLog(std::numbers::sqrt3_v<float>); // 1.73205
 
// 1 divided by the square root of 3
DebugLog(std::numbers::inv_sqrt3_v<float>); // 0.57735
 
// The Euler-Mascheroni constant
DebugLog(std::numbers::egamma_v<float>); // 0.577216
 
// The golden ratio
DebugLog(std::numbers::phi_v<float>); // 1.61803

For convenience, and as in C#, versions with simplified naming are provided for double:

DebugLog(std::numbers::pi); // 3.14159
Numeric

We’ll cover the <numeric> header in two parts because it serves two quite different purposes. Today we’ll just look at three common numeric algorithms it provides. These aren’t available in C#:

#include <numeric>
 
// Greatest common divisor
DebugLog(std::gcd(12, 9)); // 3
 
// Least common multiple
DebugLog(std::lcm(12, 9)); // 36
 
// Half way between two numbers
DebugLog(std::midpoint(12.0, 9.0)); // 10.5

We’ll see the rest of the <numeric> header, which deals with sequences of numbers, later in the series when we look at generic algorithms.

Ratio

The <ratio> header provides a single class template: std::ratio. It takes two integer template parameters representing a numerator and a denominator. It has only two members, num and den, and both are static. These are calculated at compile time by dividing the template parameters by their greatest common divisor:

#include <ratio>
 
// Greatest common divisor of 1000 and 60 is 20
using MsPerFrame = std::ratio<1000, 60>;
 
// num = 1000 / 20 = 50
// den = 60 / 20 = 3
DebugLog(MsPerFrame::num, MsPerFrame::den); // 50, 3

A bunch of SI ratios are provided to represent powers of 10. Here are a few of them:

#include <ratio>
 
DebugLog(std::nano::num, std::nano::den); // 1, 1000000000
DebugLog(std::milli::num, std::milli::den); // 1, 1000
DebugLog(std::kilo::num, std::kilo::den); // 1000, 1
DebugLog(std::mega::num, std::mega::den); // 1000000, 1

The durations we saw in the <chrono> header are actually instantiations of std::ratio. For example:

Alias Ratio
std::chrono::seconds std::ratio<1, 1>
std::chrono::minutes std::ratio<60, 1>
std::chrono::hours std::ratio<3600, 1>
std::chrono::days std::ratio<86400, 1>

As C# lacks support for integer type parameters to its generic structs and classes, there’s no equivalent to this.

Complex

Both languages have support for complex numbers. C# has the System.Numerics.Complex struct and C++ has the std::complex class template in <complex>. That class template has specializations for at least float, double, and long double while the C# version supports only double.

Here’s how to use std::complex:

#include <complex>
 
// Real part is 2. Imaginary part is 0.
std::complex<float> c1{ 2, 0 };
DebugLog(c1.real(), c1.imag()); // 2, 0
 
// Real part is 0. Imaginary part is 1.
std::complex<float> c2{ 0, 1 };
 
// Some operators are overloaded
DebugLog(c1 + c2); // 2, 1
DebugLog(c1 - c2); // 2, -1
DebugLog(c1 == c2); // false
DebugLog(c1 != c2); // true
DebugLog(-c1); // -2, -0
 
// Trigonometric functions
DebugLog(std::sin(c1)); // 0.909297, -0
DebugLog(std::cos(c1)); // -0.416147,-0
 
// Hyperbolic functions
DebugLog(std::sinh(c1)); // 3.62686, 0
DebugLog(std::cosh(c1)); // 3.7622, 0
 
// Exponential functions
DebugLog(std::pow(c1, c2)); // 0.769239, 0.638961
DebugLog(std::sqrt(c1)); // 1.41421, 0
 
// Misc functions
DebugLog(std::abs(c1)); // 2
DebugLog(std::norm(c1)); // 4
DebugLog(std::conj(c1)); // 2, -0

The above is just a sampling of the std::complex functionality. Like the C# Complex type, quite a bit more is available. C++ also provides user-defined literals in the std::literals::complex_literals namespace to create complex numbers with 0 for the real part:

#include <complex>
 
using namespace std::literals::complex_literals;
 
std::complex<double> d = 2i;
DebugLog(d); // 0, 2
 
std::complex<float> f = 2if;
DebugLog(f); // 0, 2
 
std::complex<long double> ld = 2il;
DebugLog(ld); // 0, 2
Bit

The <bit> header, introduced in C++20, provides one enumeration for dealing with endianness. This can be used like the BitConverter.IsLittleEndian constant in C#:

#include <bit>
 
bool isLittleEndian = std::endian::native == std::endian::little;
DebugLog(isLittleEndian); // Maybe true

Mainly, this header has functions for performing bit manipulation on integer types:

#include <bit>
 
// Check if only one bit is set, i.e. value is a power of two
DebugLog(std::has_single_bit(2u)); // true
DebugLog(std::has_single_bit(3u)); // false
 
// Get the largest power of two greater than or equal to a value
DebugLog(std::bit_ceil(100u)); // 128
 
// Rotate bits left, wrapping around
DebugLog(
    std::rotl(0b10100000000000000000000000000000, 2)
           == 0b10000000000000000000000000000010); // true
 
// Count consecutive zero bits starting at the least-significant
DebugLog(std::countr_zero(0b1000u)); // 3
 
// Count total one bits
DebugLog(std::popcount(0b10101010101010101010101010101010)); // 16
 
// Reinterpret the bits of one type as another type
// Not a real cast, just a function with "cast" in the name
uint32_t i = std::bit_cast<uint32_t>(3.14f);
DebugLog(i); // 1078523331

C# doesn’t provide any of these functions, so the closest equivalent would be our own custom implementations of them.

Random

The final numeric header for today is perhaps the most interesting: <random>. Like the Random class in C#, this header provides functionality for generating random numbers. It is, however, far more advanced than its C# counterpart. For starters, multiple “engines” are available as opposed to the single algorithm that Random uses in C#. Here’s one of them:

#include <random>
 
// "Subtract with carry" algorithm for uint32_t values with parameters
std::subtract_with_carry_engine<uint32_t, 24, 10, 24> swc{};
 
// Generate random numbers
DebugLog(swc()); // Maybe 15039276
DebugLog(swc()); // Maybe 16323925
DebugLog(swc()); // Maybe 14283486
 
// Advance the engine state 100 steps without getting any numbers
swc.discard(100);
 
// Reset the seed
swc.seed(123);
 
// Some "subtract with carry" engines with common types and parameters
std::ranlux24_base r24; // 32-bit
std::ranlux48_base r48; // 64-bit

There are two more available:

#include <random>
 
// "Mersenne Twister" engines
std::mersenne_twister_engine<
    uint32_t, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13> mt{}; // Custom
std::mt19937 mt32{}; // 32-bit with common parameters
std::mt19937_64 mt64{}; // 64-bit with common parameters
 
// "Linear congruential generator" engines
std::linear_congruential_engine<uint32_t, 1, 2, 3> lce{}; // Custom
std::minstd_rand0 msr0; // 32-bit "Minimal standard"
std::minstd_rand msr1; // New version of 32-bit "Minimal standard"

There are also some “adapter” engines. These use an underlying engine rather than generating their own random numbers:

#include <random>
 
// std::mt19937 is the underlying engine
// For each block of 32 random numbers, keep 2 of them
std::discard_block_engine<std::mt19937, 32, 2> db{};
uint32_t dbr = db();
 
// std::mt19937_64 is the underlying engine generating 64-bit numbers
// Convert them to 32-bit uint32_t values
std::independent_bits_engine<std::mt19937_64, 32, uint32_t> ib{};
uint32_t ibr = ib();
 
// std::mt19937 is the underlying engine
// Keep a table of 16 random numbers and shuffle the order returned
std::shuffle_order_engine<std::mt19937, 16> so{};
uint32_t sor = so();
 
// Alias of std::shuffle_order_engine<std::minstd_rand0, 256>
std::knuth_b kb{};

A std::random_device class is also available to provide non-deterministic random numbers on systems that have hardware to produce these. If no hardware is available, a platform-dependent pseudo-random number generator is used instead:

#include <random>
 
std::random_device rd{};
DebugLog(rd()); // Maybe 448041643
DebugLog(rd()); // Maybe 1317373389
DebugLog(rd()); // Maybe 393151656

None of these are typically used directly. That’s because they return numbers on their full range of values. We usually want to generate random numbers on some particular range, so we use one of many “distribution” classes. These classes can also shape the random numbers to fit certain patterns:

#include <random>
 
// Random number generator engine
std::mt19937 engine{};
 
// Normal/Gaussian distribution of float values
// The mean is 3 and the standard deviation is 1.5
std::normal_distribution<float> distribution{ 3.0f, 1.5f };
 
// Generate random numbers with the engine on the distribution
DebugLog(distribution(engine)); // Maybe 3.37974
DebugLog(distribution(engine)); // Maybe 2.56017
DebugLog(distribution(engine)); // Maybe 3.12689

20 distribution classes are available to suit a wide variety of needs. Here are a few of them:

#include <random>
 
// Uniform distribution of float values between -1 and 1
std::uniform_real_distribution<float> urd{ -1, 1 };
 
// Distribution of bool values returning true 75% of the time
std::bernoulli_distribution bd{ 0.75f };
 
// Gamma distribution of float values with alpha and beta of 1
std::gamma_distribution<float> gd{ 1.0f, 1.0f };
 
// Distribution of int32_t values that are 0, 1, 2, or 3
// With weights of 3.1, 2.2, 1.6, and 3.4, respectively
std::discrete_distribution<int32_t> dd{3.1f, 2.2f, 1.6f, 3.4f};
Conclusion

C++ has a full-featured numerics library. At its most basic there are typed number constants in <limits> and <numbers> that expand on C# functionality like int.MaxValue by adding more constants and fleshing out the offerings so they’re available for every type.

The <numeric> and <bit> headers provide common functions relating to numbers. We can compute the Greatest Common Denominator or the number of ones in an integer. Basic implementations may be easy to write, but the Standard Library implementations are robust, well-tested, optimized, and standardized.

In <complex> and <ratio> we find some class types to help us work with pairs of numbers, be they real and imaginary or numerator and denominator. In the case of std::complex, we get similar functionality as the C# Complex type but templates enable support for float and long double in addition to just double. With std::ratio we have an easy way to represent ratios like kilo and seconds at compile time and use them to generate safer, more efficient number conversions.

Finally, there’s <random> and its suite of random number generation tools. Not only do we get a single algorithm with a few basic tools, as in C#’s Random class, but also a full suite of customizable engines, distributions, and even access to hardware-based random number generators.