Skip to content
C++
Library
since C++20
Basic

std::numbers — Mathematical Constants

C++20 compile-time mathematical constants (π, e, φ, √2, and more) in <numbers>, available as double aliases and as precision-preserving variable templates.

std::numberssince C++20

A C++20 namespace that exposes eleven mathematical constants — including π, e, φ, √2, and the Euler–Mascheroni constant — as inline constexpr values declared in <numbers>, each available both as a double alias and as a floating-point variable template.

Overview

Before C++20, portable access to mathematical constants was genuinely awkward. M_PI is a POSIX extension absent from the C++ standard; MSVC omits it unless _USE_MATH_DEFINES is defined, and relying on it in portable code invites subtle build failures. The alternative — scattering literals like 3.14159265358979323846 through the codebase — is error-prone and hard to audit.

C++20 standardised eleven constants in the std::numbers namespace inside <numbers>. Every constant follows a two-name convention:

  • std::numbers::X — a double alias, shorthand for std::numbers::X_v<double>.
  • std::numbers::X_v<T> — a variable template constrained to floating-point types (float, double, long double, or extended floating-point types). This is the canonical form.

Both forms are inline constexpr, fully evaluated at compile time with zero runtime cost. The variable template X_v<T> is constrained to std::floating_point<T> (a C++20 concept); passing an integral type is a hard compile error.

The complete set introduced in C++20:

ConstantMathematical meaningApproximate value
eEuler's number (base of natural log)2.718 281 828…
log2elog₂(e)1.442 695 040…
log10elog₁₀(e)0.434 294 481…
piπ3.141 592 653…
inv_pi1/π0.318 309 886…
inv_sqrtpi1/√π0.564 189 583…
sqrt2√21.414 213 562…
sqrt3√31.732 050 807…
inv_sqrt31/√30.577 350 269…
egammaEuler–Mascheroni constant (γ)0.577 215 664…
phiGolden ratio φ = (1 + √5)/21.618 033 988…

Syntax

cpp
#include <numbers>     // C++20

// Double-precision aliases
double a = std::numbers::pi;       // 3.14159265358979...
double b = std::numbers::e;        // 2.71828182845904...
double c = std::numbers::sqrt2;    // 1.41421356237309...
double d = std::numbers::phi;      // 1.61803398874989...
double g = std::numbers::egamma;   // 0.57721566490153...

// Variable templates — match precision to the computation type
float       pi_f  = std::numbers::pi_v<float>;
double      pi_d  = std::numbers::pi_v<double>;
long double pi_ld = std::numbers::pi_v<long double>;

Variable Template vs Double Alias: Why Precision Matters

The bare alias std::numbers::pi resolves to std::numbers::pi_v<double>, which holds the double-precision value 3.141592653589793238…. When you use it inside a function templated on T = float, the compiler silently widens the float operands to double, performs the multiplication in double precision, then narrows the result back to float. That round-trip is not merely wasteful — it can produce results that differ from a clean, consistently float-precision computation by a full ULP.

std::numbers::pi_v<float> holds the best float approximation of π: 3.14159274… (rounded from double-precision), which is distinct from the truncated double value. Use pi_v<T> to ensure the compiler selects the correctly-rounded constant for the target type, with no implicit conversions anywhere in the expression.

Concrete physics example: measurable float vs double error

The angular frequency of a simple harmonic oscillator is ω = 2πf. A naive implementation using the double alias in a float-typed function accumulates rounding error across a large array of frequencies:

cpp
#include <cmath>
#include <numbers>
#include <print>    // C++23

// BAD: uses double alias — widening conversion on every call
float angular_frequency_bad(float freq_hz) noexcept {
    return 2.0f * std::numbers::pi * freq_hz;   // std::numbers::pi is double
    //                             ^^^^^^^^^
    //  freq_hz is promoted to double, multiplied by pi_v<double>,
    //  result narrowed back to float — one implicit conversion per call
}

// GOOD: stays entirely in float
float angular_frequency_good(float freq_hz) noexcept {
    return 2.0f * std::numbers::pi_v<float> * freq_hz;  // C++20
}

int main() {
    float f = 440.0f;   // concert A

    float omega_bad  = angular_frequency_bad(f);
    float omega_good = angular_frequency_good(f);

    // Both print "2764.601562" — but they may differ by 1 ULP
    // (difference is ~2.4e-4 at this magnitude, which matters in DSP)
    std::println("double-path:  {:.9f}", omega_bad);
    std::println("float-path:   {:.9f}", omega_good);

    // The ULP difference becomes visible when summing many samples
    double accumulated_bad  = 0.0;
    double accumulated_good = 0.0;
    for (int i = 0; i < 1'000'000; ++i) {
        accumulated_bad  += angular_frequency_bad(f);
        accumulated_good += angular_frequency_good(f);
    }
    std::println("Accumulated error over 1M calls: {:.6f}",
        accumulated_bad - accumulated_good);  // non-zero
}

The difference is tiny per call, but in a signal-processing loop over millions of samples the accumulated ULP mismatch causes audible phase drift — a real bug that is invisible to code review without knowing the type of std::numbers::pi.

Precision Comparison Table

The table below shows the exact stored bitpattern (as a decimal literal) for each precision of π, and how many ULPs each departs from the true mathematical value. "ULP error" here means the distance to the nearest representable value beyond the rounded constant.

TypeStored valueTrue πULP error
float (binary32)3.1415927410…3.14159265358979…0 (correctly rounded)
double (binary64)3.14159265358979323…3.14159265358979…0 (correctly rounded)
long double (80-bit x87)3.141592653589793238464…3.14159265358979…0 (correctly rounded)

Every stored value is the closest representable value in that format — each pi_v<T> is the correctly-rounded constant for type T. The key takeaway is that pi_v<float> ≠ (float)pi_v<double>: the correctly-rounded float approximation of π is 3.14159274… (0x40490FDB), while simply casting the double value gives the same bit pattern. However, intermediate precision in mixed-type expressions matters for the result type, not just the constant — keeping constant and operands at the same type avoids unexpected rounding.

Extended Floating-Point Types (C++23)

The template constraint on X_v<T> is std::floating_point<T>, which is satisfied by any type that models a floating-point number — including the optional <stdfloat> types introduced in C++23. Where the hardware and implementation support them, you can instantiate pi_v directly on std::float16_t, std::float32_t, std::float64_t, std::float128_t, and std::bfloat16_t.

cpp
#include <numbers>    // C++20
#include <stdfloat>   // C++23 — optional types

#if defined(__STDCPP_FLOAT16_T__)
    std::float16_t pi16 = std::numbers::pi_v<std::float16_t>;   // C++23
    // Stored as IEEE binary16: 3.140625 (nearest half-precision value)
#endif

#if defined(__STDCPP_FLOAT32_T__)
    std::float32_t pi32 = std::numbers::pi_v<std::float32_t>;   // C++23
    // Identical to pi_v<float> on platforms where float32_t aliases float
#endif

#if defined(__STDCPP_FLOAT128_T__)
    std::float128_t pi128 = std::numbers::pi_v<std::float128_t>; // C++23
    // ~34 significant decimal digits; useful for extended-precision geometry
#endif

The guard macros (__STDCPP_FLOAT16_T__ etc.) are defined by the implementation when the corresponding type is available. Without them the template instantiation is a compile error, so always guard behind the availability macro in portable code.

The practical interest in float16_t / bfloat16_t variants comes from machine-learning inference kernels, where entire pipelines operate in half precision and every intermediate constant must match the operand type to avoid silent widening. Using pi_v<std::float16_t> in a GEMM-based FFT kernel is the same idea as pi_v<float> — keep the constant in the computation's native type.

Examples

Angle conversion and trigonometry

cpp
#include <cmath>
#include <numbers>
#include <print>      // C++23

constexpr double to_radians(double degrees) noexcept {
    return degrees * (std::numbers::pi / 180.0);
}

constexpr double to_degrees(double radians) noexcept {
    return radians * (180.0 / std::numbers::pi);
}

int main() {
    // Angle of elevation: 100ft high steeple, 50ft away
    double angle_rad = std::atan2(100.0, 50.0);
    std::println("Steeple angle: {:.4f}°", to_degrees(angle_rad));  // 63.4349°

    // Verify cos(45°) via the conversion
    std::println("cos(45°) = {:.6f}", std::cos(to_radians(45.0)));  // 0.707107
}

Physics and math patterns in generic code

Always use pi_v<T> in templates. The pattern generalises cleanly to any geometry or wave formula. The generic functions below work correctly for float, double, and long double without a single cast or implicit conversion:

cpp
#include <cmath>
#include <concepts>    // C++20
#include <numbers>

// Circumference: C = 2πr
template <std::floating_point T>    // C++20
constexpr T circumference(T radius) noexcept {
    return T{2} * std::numbers::pi_v<T> * radius;
}

// Area of circle: A = πr²
template <std::floating_point T>
constexpr T circle_area(T radius) noexcept {
    return std::numbers::pi_v<T> * radius * radius;
}

// Volume of sphere: V = (4/3)πr³
template <std::floating_point T>
constexpr T sphere_volume(T radius) noexcept {
    return (T{4} / T{3}) * std::numbers::pi_v<T> * radius * radius * radius;
}

// Simple wave function: y = A · sin(2π · f · t)
template <std::floating_point T>
constexpr T wave(T amplitude, T freq_hz, T time_s) noexcept {
    return amplitude * std::sin(T{2} * std::numbers::pi_v<T> * freq_hz * time_s);
}

// All usable at compile time
static_assert(circle_area(1.0f)  > 3.14f);
static_assert(sphere_volume(1.0) > 4.18);
static_assert(circumference(1.0L) > 6.28L);

// float computation stays entirely in float — no implicit widening
float pond_area = 78.5f;
float diameter  = 2.0f * std::sqrt(pond_area / std::numbers::pi_v<float>);

The static_assert calls verify all three instantiations at compile time. Because the function arguments are constexpr literals, the entire computation — including the constant lookup — is constant-folded by the compiler.

Efficient logarithm base change

log2e and log10e are multiplication-ready reciprocals of common logarithm bases. They replace a runtime division by std::log(2) or std::log(10) with a compile-time constant multiply:

cpp
#include <cmath>
#include <numbers>

// log₂(x) = ln(x) * log₂(e)   — constant multiply, no division
double fast_log2(double x) noexcept {
    return std::log(x) * std::numbers::log2e;
}

// log₁₀(x) = ln(x) * log₁₀(e)
double fast_log10(double x) noexcept {
    return std::log(x) * std::numbers::log10e;
}

This is numerically equivalent to dividing by the respective log, but the compiler folds the constant multiplication into a single FP instruction.

Euler–Mascheroni constant and harmonic series

γ appears in asymptotic analysis of harmonic numbers: H(n) = 1 + 1/2 + … + 1/n satisfies H(n) ≈ ln(n) + γ + 1/(2n). This approximation is tight even for modest n:

cpp
#include <cmath>
#include <numbers>
#include <print>    // C++23

double harmonic_approx(int n) noexcept {
    double nd = static_cast<double>(n);
    return std::log(nd) + std::numbers::egamma + 1.0 / (2.0 * nd);
}

int main() {
    // Exact H(10) = 7381/2520 ≈ 2.928968
    std::println("H(10)  approx: {:.6f}", harmonic_approx(10));   // 2.929177
    std::println("H(100) approx: {:.6f}", harmonic_approx(100));  // 5.187378
}

Golden ratio and Fibonacci growth

The golden ratio φ = (1 + √5)/2 ≈ 1.618 governs the closed-form expression for Fibonacci numbers (Binet's formula): F(n) = (φⁿ − ψⁿ) / √5, where ψ = 1 − φ = (1 − √5)/2 ≈ −0.618. Beyond the formula, φ can be used to model exponential growth in any population whose growth factor converges to the golden ratio:

cpp
#include <cmath>
#include <numbers>
#include <print>    // C++23

// Binet's formula — exact for all n where double precision suffices (n ≤ ~70)
long long fibonacci(int n) noexcept {
    double phi = std::numbers::phi;                  // C++20: (1+√5)/2
    double psi = 1.0 - std::numbers::phi;            // (1-√5)/2 ≈ -0.618
    return static_cast<long long>(
        std::round((std::pow(phi, n) - std::pow(psi, n)) / std::numbers::sqrt5)
        // sqrt5 is not in std::numbers; compute it:
    );
}

// Or use sqrt5 = phi * 2 - 1 (derived from phi definition)
constexpr double sqrt5 = 2.0 * std::numbers::phi - 1.0;   // C++20: √5

// Model population whose growth rate converges toward φ
//   P(t) = P0 * phi^t   (continuous analogue; t in "generations")
double fibonacci_growth(double initial_population, double generations) noexcept {
    return initial_population * std::pow(std::numbers::phi, generations);
}

int main() {
    std::println("Fib(10) via growth model: {:.1f}", fibonacci_growth(1.0, 10.0));
    // Approaches F(10) = 55 as the psi term vanishes

    std::println("√5 from phi: {:.15f}", sqrt5);   // 2.236067977499790
    std::println("φ²  = φ+1:  {:.15f}",
        std::numbers::phi * std::numbers::phi);     // 2.618... = 1.618... + 1
}

Note that std::numbers does not include √5 directly; the relation sqrt5 = 2φ − 1 gives it from phi at compile time without std::sqrt.

Why Not Just Use M_PI?

Three options are in common use across real codebases. Here is how they compare:

OptionPortabilityPrecisionIntent
M_PIPOSIX extension only; absent on MSVC without _USE_MATH_DEFINES; not in the C++ standardSame as double (implementation-defined precision)Implicit — unclear which type the caller expects
3.14159265358979323846Fully portable — just a literalAs many significant digits as the type can hold (literal is 21 digits, enough for long double)No intent — reader must count digits to identify the constant
std::numbers::piStandard C++20; any conforming compilerExactly pi_v<double> — correctly rounded doubleClear — the name says what it is and which type
std::numbers::pi_v<T>Standard C++20; sameCorrectly rounded for type TIdeal — constant and type are unified
cpp
// Avoid: non-portable, requires preprocessor guard on MSVC
#define _USE_MATH_DEFINES
#include <cmath>
double area = M_PI * r * r;

// Avoid: magic literal, no type safety, easy to mis-type
double area = 3.14159265358979323846 * r * r;

// Good: standard, typed, zero runtime cost
#include <numbers>   // C++20
double area = std::numbers::pi * r * r;

// Best in generic code: stays in the caller's chosen type
template <std::floating_point T>
T area(T r) { return std::numbers::pi_v<T> * r * r; }

The strongest reason to prefer std::numbers::pi over the literal is maintainability: future readers can grep for pi, see exactly what was intended, and the compiler enforces the type. The literal is silent about its meaning and requires a human to recognise those digits.

Compile-Time Computations

All std::numbers constants are inline constexpr, which means they participate fully in constant evaluation. They can appear in constexpr function bodies, non-type template arguments, static_assert conditions, and array-size expressions.

cpp
#include <numbers>   // C++20

// Derived compile-time constants — no runtime cost, ODR-safe
inline constexpr double two_pi      = 2.0 * std::numbers::pi;
inline constexpr double half_pi     = std::numbers::pi / 2.0;
inline constexpr double rad_per_deg = std::numbers::pi / 180.0;
inline constexpr double deg_per_rad = 180.0 / std::numbers::pi;

// constexpr function using the constant
constexpr double circle_circumference(double r) noexcept {
    return two_pi * r;
}

// static_assert: verify at compile time, not test-time
static_assert(circle_circumference(1.0) > 6.28 &&
              circle_circumference(1.0) < 6.29,
              "circumference of unit circle must be 2π");

// Use in a non-type template argument (C++20 allows floating-point NTTPs)
template <double Freq>
struct Oscillator {
    static constexpr double angular_freq = 2.0 * std::numbers::pi * Freq;
    // angular_freq is computed at instantiation time, not at runtime
};

constexpr auto osc440 = Oscillator<440.0>{};
static_assert(osc440.angular_freq > 2764.0);

The key insight is that because std::numbers::pi is constexpr, expressions derived from it — two_pi, rad_per_deg, and so on — are also constexpr without any consteval or constinit annotation. The compiler constant-folds all arithmetic that involves only constexpr operands.

cpp
// Feature-test macro: guard C++17 fall-through
#if __cpp_lib_math_constants >= 201907L
#  include <numbers>
   inline constexpr double kPi = std::numbers::pi;    // C++20
#else
   inline constexpr double kPi = 3.141592653589793238462643383;
#endif

The feature-test macro __cpp_lib_math_constants is 201907L when the <numbers> header is available; this pattern lets a single header compile under both C++17 and C++20.

Best Practices

Always use pi_v<T> in templates. std::numbers::pi is double. Inside a function templated on a floating-point type T, using the bare alias forces implicit conversion; pi_v<T> keeps arithmetic in the caller's chosen type at no cost.

Prefer these over M_PI unconditionally. M_PI requires non-portable preprocessor guards and is absent by default on MSVC. std::numbers::pi is always available under C++20 with no configuration required.

Define composite constants once, not inline. If your code repeatedly needs 2π or 180/π, define them in a single header rather than computing them at every call site:

cpp
// Portable, ODR-safe, zero runtime cost
inline constexpr double two_pi      = 2.0 * std::numbers::pi;    // C++20
inline constexpr double half_pi     = std::numbers::pi / 2.0;
inline constexpr double rad_per_deg = std::numbers::pi / 180.0;
inline constexpr double deg_per_rad = 180.0 / std::numbers::pi;

Guard C++17 fall-through with feature-test macros. Code that must remain compatible with C++17 can test for the feature before using it:

cpp
#if __cpp_lib_math_constants >= 201907L
#  include <numbers>
   inline constexpr double kPi = std::numbers::pi;
#else
   inline constexpr double kPi = 3.141592653589793238462643383;
#endif

Guard <stdfloat> types behind their availability macros. pi_v<std::float16_t> is only valid when __STDCPP_FLOAT16_T__ is defined; instantiating it without the guard is a compile error on platforms that lack the type.

Common Pitfalls

Wrong header. <cmath> does not pull in <numbers>. Referencing std::numbers::pi without #include <numbers> is a compile error.

Implicit narrowing via the double alias.

cpp
float r    = 2.5f;
float area = std::numbers::pi * r * r;  // std::numbers::pi is double;
                                         // result is double, silently narrowed to float

Write std::numbers::pi_v<float> to keep the entire computation in float.

Redefining constants as macros. Writing #define PI std::numbers::pi defeats namespacing, breaks in contexts where std:: is not visible, and prevents the compiler from diagnosing type mismatches. Use the namespace name directly.

Using inv_sqrt3 when you want the wrong thing. std::numbers::inv_sqrt3 equals 1/√3 ≈ 0.5774. It does not equal 1/3 ≈ 0.3333. The name is compact but the meaning requires a moment's thought; if the computation isn't obvious at the call site, bind to a named local variable.

Assuming sqrt5 is available. There is no std::numbers::sqrt5. If you need it, derive it: constexpr double sqrt5 = 2.0 * std::numbers::phi - 1.0; (exact, since φ = (1 + √5)/2 implies √5 = 2φ − 1).

See Also

  • <cmath> — runtime floating-point functions (std::sin, std::log, std::sqrt) that consume these constants
  • <complex> — complex arithmetic operating on the same floating-point types
  • <numeric> — std::midpoint, std::lerp, and other numeric utilities (C++20)
  • <stdfloat> — optional extended floating-point types compatible with pi_v<T> (C++23)
  • Constant expressions — the constexpr evaluation model that makes inline constexpr constants zero-cost