C++ was not designed primarily with numeric computation in mind — but numeric computation typically occurs in the context of other work (scientific computation, database access, networking, instrument control, graphics, simulation, financial analysis), so C++ becomes an attractive vehicle for computations that are part of a larger system. Where more complex data structures are needed as part of a computation, C++'s strengths become relevant. This chapter describes the parts of the standard library that support numerics: mathematical functions, numerical algorithms, complex numbers, random numbers, vector arithmetic, numeric limits, type aliases, and mathematical constants. The thread running through it: the library already knows how to do numerics correctly — subtle, and easy to get wrong by hand.
Why it matters: the standard library's numeric components are how you do
serious computation inside a larger program — accumulate() and friends instead
of hand-rolled sum loops (the algorithm idiom of Chapter 13 applied to numbers), engines
bound to distributions instead of the C rand(),
std::complex instead of hand-managed real/imaginary pairs, and
numeric_limits + sized aliases instead of hoping an int is big
enough. The advice is blunt:
don't do serious numeric computation with only the bare language — use libraries.
C++ is widely used for scientific, engineering, financial, and other computation involving sophisticated numerics; consequently, facilities and techniques supporting such computation have emerged. Numerical problems are often subtle: if you are not 100% certain about the mathematical aspects of a numerical problem, either take expert advice, experiment, or do both.
In <cmath>, we find the standard mathematical functions, such as
sqrt(), log(), and sin(), for arguments of type
float, double, and long double:
| Function | Meaning |
|---|---|
abs(x) |
Absolute value |
ceil(x) |
Smallest integer >= x |
floor(x) |
Largest integer <= x |
sqrt(x) |
Square root; x must be non-negative |
cos(x) |
Cosine |
sin(x) |
Sine |
tan(x) |
Tangent |
acos(x) |
Arccosine; the result is non-negative |
asin(x) |
Arcsine; the result nearest to 0 is returned |
atan(x) |
Arctangent |
sinh(x) |
Hyperbolic sine |
cosh(x) |
Hyperbolic cosine |
tanh(x) |
Hyperbolic tangent |
exp(x) |
Base e exponential |
exp2(x) |
Base 2 exponential |
log(x) |
Natural logarithm, base e; x must be positive |
log2(x) |
Natural logarithm, base 2; x must be positive |
log10(x) |
Base 10 logarithm; x must be positive |
The versions of these functions for complex numbers (§17.4) are found in
<complex>. For each function, the return type is the same as the argument
type. Errors are reported by setting errno from <cerrno> to
EDOM for a domain error and to ERANGE for a range error:
errno = 0; // clear old error state
double d = sqrt(-1);
if (errno==EDOM) cerr << "sqrt() not defined for negative argument\n";
errno = 0; // clear old error state
double dd = pow(numeric_limits<double>::max(),2);
if (errno == ERANGE) cerr << "result of pow() too large to represent as a double\n";
More mathematical functions are found in <cmath> and
<cstdlib>. The so-called special mathematical functions, such as
beta(), riemann_zeta(), and sph_bessel(), are also in
<cmath>.
In <numeric>, we find a small set of generalized numerical algorithms, such
as accumulate():
| Algorithm | Meaning |
|---|---|
x=accumulate(b,e,i) |
x is the sum of i and the elements of [b:e) |
x=accumulate(b,e,i,f) |
accumulate using f instead of + |
x=inner_product(b,e,b2,i) |
x is the inner product of [b:e) and [b2:b2+(e-b)) — the sum of i and (*p1)*(*p2) for each p1 and corresponding p2 |
x=inner_product(b,e,b2,i,f,f2) |
inner_product using f and f2 instead of + and * |
p=partial_sum(b,e,out) |
Element i of [out:p) is the sum of elements [b:b+i] |
p=partial_sum(b,e,out,f) |
partial_sum using f instead of + |
p=adjacent_difference(b,e,out) |
Element i of [out:p) is *(b+i)-*(b+i-1) for i>0; if e-b>0, then *out is *b |
p=adjacent_difference(b,e,out,f) |
adjacent_difference using f instead of - |
iota(b,e,v) |
For each element in [b:e) assign v and increment ++v; the sequence becomes v, v+1, v+2, ... |
x=gcd(n,m) |
x is the greatest common denominator of integers n and m |
x=lcm(n,m) |
x is the least common multiple of integers n and m |
x=midpoint(n,m) |
x is the midpoint between n and m |
These algorithms generalize common operations such as computing a sum by letting them apply to all kinds of sequences; they also make the operation applied to the elements a parameter. For each algorithm, the general version is supplemented by a version applying the most common operator for that algorithm:
list<double> lst {1, 2, 3, 4, 5, 9999.99999};
auto s = accumulate(lst.begin(),lst.end(),0.0); // calculate the sum: 10014.9999
These algorithms work for every standard-library sequence and can have operations supplied as arguments.
The numerical algorithms have parallel versions that differ slightly from the sequential ones
— in particular, the parallel versions allow operations on elements in
unspecified order. The parallel numerical algorithms can take an execution policy
argument (§13.6): seq, unseq, par, and
par_unseq, hidden in namespace std::execution in
<execution>.
| Parallel Algorithm | Meaning |
|---|---|
x=reduce(b,e,v) |
x=accumulate(b,e,v), except out of order |
x=reduce(b,e) |
x=reduce(b,e,V{}), where V is b's value type |
x=reduce(pol,b,e,v) |
x=reduce(b,e,v) with execution policy pol |
p=exclusive_scan(pol,b,e,out) |
p=partial_sum(b,e,out) according to pol, excludes the ith input element from
the ith sum
|
p=inclusive_scan(pol,b,e,out) |
p=partial_sum(b,e,out) according to pol, includes the ith input element in
the ith sum
|
p=transform_reduce(pol,b,e,f,v) |
f(x) for each x in [b:e), then reduce |
p=transform_exclusive_scan(pol,b,e,out,f,v) |
f(x) for each x in [b:e), then exclusive_scan |
p=transform_inclusive_scan(pol,b,e,out,f,v) |
f(x) for each x in [b:e), then inclusive_scan |
Just as for the parallel algorithms in <algorithm>
(§13.6), we can specify an execution policy:
vector<double> v {1, 2, 3, 4, 5, 9999.99999};
auto s = reduce(v.begin(),v.end()); // calculate the sum using a double as the accumulator
vector<double> large; // ... fill large with lots of values ...
auto s2 = reduce(par_unseq,large.begin(),large.end()); // calculate the sum using available parallelism
Measure to verify that using a parallel or vectorized algorithm is worthwhile.
The standard library supports a family of complex number types along the lines of the complex
class described in §5.2.1. To support complex numbers where the scalars are single-precision
(floats), double-precision (doubles), etc., the standard-library
complex is a template:
template<typename Scalar> class complex {
public:
complex(const Scalar& re ={}, const Scalar& im ={}); // default function arguments; see §3.4.1
// ...
};
The usual arithmetic operations and the most common mathematical functions are supported for complex numbers:
void f(complex<float> fl, complex<double> db) {
complex<long double> ld {fl+sqrt(db)};
db += fl * 3;
fl = pow(1/fl,2);
// ...
}
The sqrt() and pow() (exponentiation) functions are among the usual
mathematical functions defined in <complex> (§17.2).
Random numbers are useful in many contexts, such as testing, games, simulation, and security.
The diversity of application areas is reflected in the wide selection of random number
generators provided by the standard library in <random>. A random number
generator consists of two parts:
Examples of distributions are uniform_int_distribution
(where all integers produced are equally likely),
normal_distribution ("the bell curve"), and
exponential_distribution (exponential growth); each for some specified range:
using my_engine = default_random_engine; // type of engine
using my_distribution = uniform_int_distribution<>; // type of distribution
my_engine eng {}; // the default version of the engine
my_distribution dist {1,6}; // distribution that maps to the ints 1..6
auto die = [&](){ return dist(eng); }; // make a generator
int x = die(); // roll the die: x becomes a value in [1:6]
Thanks to its uncompromising attention to generality and performance, one expert has deemed
the standard-library random number component "what every random number library wants to be
when it grows up" — however, it can hardly be deemed "novice friendly." The
using statements and the lambda make what is being done a bit more obvious. For
novices, a simple uniform random number generator is often sufficient to get started:
Rand_int rnd {1,10}; // make a random number generator for [1:10]
int x = rnd(); // x is a number in [1:10]
To get that, we combine an engine with a distribution inside a class:
class Rand_int {
public:
Rand_int(int low, int high) :dist{low,high} { }
int operator()() { return dist(re); } // draw an int
void seed(int s) { re.seed(s); } // choose new random engine seed
private:
default_random_engine re;
uniform_int_distribution<> dist;
};
That definition is still "expert level," but the use of Rand_int() is
manageable in the first week of a C++ course for novices — e.g., a histogram of 200 draws from
Rand_int rnd {0,max} produces a reassuringly boring uniform distribution.
To get a repeated or different sequence of values, we seed the engine — that is, give its internal state a new value:
Rand_int rnd {10,20};
for (int i = 0; i<10; ++i) cout << rnd() << ' '; // 16 13 20 19 14 17 10 16 15 14
rnd.seed(999);
for (int i = 0; i<10; ++i) cout << rnd() << ' '; // 11 17 14 19 20 13 20 14 16 19
rnd.seed(999); // same seed, same sequence
for (int i = 0; i<10; ++i) cout << rnd() << ' '; // 11 17 14 19 20 13 20 14 16 19
Repeated sequences are important for deterministic debugging; seeding with
different values is important when we don't want repetition. If you need genuine random
numbers, rather than a generated pseudo-random sequence, look to see how
random_device is implemented on your machine.
The vector described in §12.2 was designed to be a general mechanism for holding
values, to be flexible, and to fit into the architecture of containers, iterators, and
algorithms — it does not support mathematical vector operations. Adding such
operations to vector would be easy, but its generality and flexibility preclude
optimizations that are often considered essential for serious numerical work. Consequently,
the standard library provides (in <valarray>) a vector-like template,
called valarray, that is
less general and more amenable to optimization for numerical computation:
template<typename T> class valarray { /* ... */ };
void f(valarray<double>& a1, valarray<double>& a2) {
valarray<double> a = a1 * 3.14+a2/a1; // numeric array operators *, +, /, and =
a2 += a1 * 3.14;
a = abs(a);
double d = a2[7];
// ...
}
The operations are vector operations — applied to each element of the vectors
involved. In addition to arithmetic operations, valarray offers
stride access to help implement multidimensional computations.
In <limits>, the standard library provides classes that describe the
properties of built-in types — such as the maximum exponent of a float or the
number of bytes in an int:
static_assert(numeric_limits<char>::is_signed,"unsigned characters!");
static_assert(100000<numeric_limits<int>::max(),"small ints!");
The second assert works because numeric_limits<int>::max() is a
constexpr function (§1.6). We can define numeric_limits for our own
user-defined types.
The size of fundamental types, such as int and long long, is
implementation-defined — they may be different on different implementations of C++. If we need
to be specific about the size of our integers, we can use aliases defined in
<stdint>, such as int32_t and uint_least64_t (an
unsigned integer with at least 64 bits). The curious _t suffix is a relic from
the days of C when it was deemed important to have a name reflect that it named an alias.
Other common aliases, such as size_t (the type returned by the
sizeof operator) and ptrdiff_t (the type of the result of
subtracting one pointer from another), can be found in <stddef>.
When doing mathematical computations, we need common mathematical constants such as
e, pi, and log2e. The standard library offers those and
more, in <numbers>. They come in two forms: a template that allows us to
specify the exact type (e.g., pi_v<T>) and a short name for the most common
use (e.g., pi meaning pi_v<double>):
void area(float r) {
using namespace std::numbers; // this is where the mathematical constants are kept
double d = pi * r * r;
float f = pi_v<float> * r * r;
// ...
}
Here the difference is small (we would have to print with precision 16 or so to see it), but
in real physics calculations such differences quickly become significant. Other areas where
the precision of constants matters are graphics and AI, where smaller representations of
values are increasingly important. In <numbers> we find
e (Euler's number), log2e, log10e, pi,
inv_pi (1/pi), inv_sqrtpi (1/sqrt(pi)), ln2,
ln10, sqrt2, sqrt3, inv_sqrt3,
egamma (the Euler-Mascheroni constant), and phi (the golden ratio).
Naturally, we would like more constants and constants for different domains — that's easily
done because such constants are variable templates with specializations for
double (or whatever type is most useful for a domain):
template<typename T> constexpr T tau_v = 2 * pi_v<T>;
constexpr double tau = tau_v<double>;
Here is a summary of the guidance from this chapter. All 12 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.
| # | Guideline | § |
|---|---|---|
| 1 | Numerical problems are often subtle. If you are not 100% certain about the mathematical aspects of a numerical problem, either take expert advice, experiment, or do both. | 17.1 |
| 2 | Don't try to do serious numeric computation using only the bare language; use libraries. | 17.1 |
| 3 |
Consider accumulate(), inner_product(),
partial_sum(), and adjacent_difference() before you write a loop
to compute a value from a sequence.
|
17.3 |
| 4 | For larger amounts of data, try the parallel and vectorized algorithms. | 17.3.1 |
| 5 | Use std::complex for complex arithmetic. |
17.4 |
| 6 | Bind an engine to a distribution to get a random number generator. | 17.5 |
| 7 | Be careful that your random numbers are sufficiently random for your intended use. | 17.5 |
| 8 |
Don't use the C standard-library rand(); it isn't sufficiently random for
real uses.
|
17.5 |
| 9 |
Use valarray for numeric computation when run-time efficiency is more
important than flexibility with respect to operations and element types.
|
17.6 |
| 10 | Properties of numeric types are accessible through numeric_limits. |
17.7 |
| 11 |
Use numeric_limits to check that the numeric types are adequate for their
use.
|
17.7 |
| 12 | Use aliases for integer types if you want to be specific about their sizes. | 17.8 |
How do the <cmath> functions report errors, and what are the argument/return rules?
What do accumulate, inner_product, partial_sum, and adjacent_difference generalize?
What does reduce do differently from accumulate, and what policies does it take?
What is std::complex, and where do its mathematical functions live?
What are the two parts of a <random> generator, and how do they combine?
Why seed an engine, and when do you want repeated sequences?
Why does valarray exist instead of adding vector operations to vector?
What does <limits> give you, and why can it be used at compile time?
Why use int32_t, and where do size_t and ptrdiff_t come from?
What two forms do the <numbers> constants come in, and why does precision matter?
Why not use C's rand(), and what's the general stance on numeric code?
Which facility fits which need: sum over a sequence, big-data parallel sum, and checking type adequacy?
<cmath> ⇒ sqrt, log, sin, cos, tan, exp, abs, ceil, floor,
hyp/arc variants | float/double/long double → return type = argument type
errors ⇒ errno (<cerrno>) |
EDOM domain (sqrt(-1)) | ERANGE range
(pow overflow)
complex versions in <complex> | special functions
⇒ beta(), riemann_zeta(), sph_bessel()
<numeric> ⇒ generalize sum/combine over any sequence, operation
as parameter
accumulate(b,e,i) / (b,e,i,f) |
inner_product two sequences | partial_sum running sums
|
adjacent_difference neighbor diffs | iota = v, v+1, v+2…
| gcd/lcm/midpoint
advice 3 ⇒ consider these before writing a loop for a value from a sequence
reduce ⇒ accumulate out of order → parallelizable
| policies ⇒ seq/unseq/par/par_unseq
(std::execution, <execution>)
scans ⇒ exclusive_scan excludes ith input from ith sum v/s
inclusive_scan includes | transform_reduce = f(x) then
reduce
measure to verify parallelism worthwhile (advice 4)
template<typename Scalar> class complex
⇒ complex<float/double/long double> | re/im args defaulted
(§3.4.1)
usual arithmetic + math functions ⇒ sqrt/pow in <complex>
| mixed scalar conversions fine
advice 5 ⇒ use std::complex for complex arithmetic
generator ⇒ engine (sequence of values) +
distribution (maps into range/shape) → call
dist(eng)
distributions ⇒ uniform_int | normal (bell curve) | exponential
die() lambda ⇒ dist {1,6}, x in [1:6] | novice-friendly
wrapper ⇒ Rand_int class
seeding ⇒ same seed = same sequence → deterministic debugging | genuine randomness ⇒ random_device (check implementation)
advice 6-8 ⇒ bind engine to distribution | enough randomness for use | no C rand()
<valarray> ⇒ vector ops elementwise (a1*3.14+a2/a1,
a=abs(a)) | less general, more optimizable v/s vector's
generality blocks optimization
stride access ⇒ multidimensional computations | advice 9 ⇒ efficiency > flexibility
numeric_limits<T> ⇒ is_signed, max() (constexpr →
static_assert) | definable for user types | advice 10-11
aliases ⇒ <stdint> int32_t, uint_least64_t |
<stddef> size_t (sizeof), ptrdiff_t (pointer
diff) | _t relic | advice 12
constants ⇒ <numbers> std::numbers |
pi_v<T> template v/s pi (= pi_v<double>)
| e, log2e, ln2, sqrt2, phi, egamma… | variable templates
→ define tau_v
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 17: "Numerics." Addison-Wesley.
Reference:
Chapter 1–12 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
<cmath>,
<numeric>, and
random number generation.
Questions? Ask your agent — your teacher — about anything unclear: why reduce can
run out of order, when to seed a random engine, or which numeric type is big enough for your
data. Follow-ups are expected, not optional.