C++ For C# Developers: Part 42 – Numbers Library
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrap-up
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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.
#1 by John on March 10th, 2021 ·
Is there a C++ library you could recommend for linear algebra and spatial transforms like Unity has built-in?
#2 by jackson on March 11th, 2021 ·
Game engines like Unreal, Cryengine, and Lumberyard that use C++ have built in functionality for this that’s similar to Unity. If you’re building your own game engine or working outside of a game engine then you may need a library. There are many to choose from including, for example DirectXMath and OpenCV depending on your needs.