Skip to content
C++
Language
since C++98
Basic

Numeric Limits and Fixed-Width Types

std::numeric_limits for compile-time type properties, <cstdint> fixed-width integers, and std::numbers math constants in C++.

std::numeric_limits<T>since C++98

A fully specialized class template in <limits> that exposes the range, precision, and IEEE 754 characteristics of every arithmetic type as constexpr members (since C++11), superseding C macros like INT_MAX and DBL_EPSILON in generic code.

Overview

std::numeric_limits<T> predates C++11 but became substantially more useful when all its static members were made constexpr in C++11 — enabling their use in template parameters, static_assert, array sizes, and constexpr functions. The C macros from <climits> and <cfloat> remain available but are non-parameterizable and cannot appear in template code.

The template is specialized for every fundamental arithmetic type: bool, all variants of char, short, int, long, long long, float, double, and long double. It is also usable with standard library aliases like std::size_t and std::ptrdiff_t since they are typedefs of arithmetic types. Unspecialized instantiations have is_specialized == false and return zero or false for all members.

Fixed-width integers from <cstdint> (C++11, drawn from C99) provide guaranteed-width storage types. The std::numbers namespace (C++20) delivers correctly rounded constexpr values for mathematical constants across floating-point precisions.

Syntax

std::numeric_limits key members

cpp
#include <limits>

// Range
std::numeric_limits<T>::min()        // most negative for integers; smallest positive normal for floats
std::numeric_limits<T>::max()        // largest finite value
std::numeric_limits<T>::lowest()     // most negative finite value (C++11 — use this as sentinel)

// Precision
std::numeric_limits<T>::digits       // mantissa bits (excl. sign bit); for integers, binary digits in value
std::numeric_limits<T>::digits10     // guaranteed significant decimal digits
std::numeric_limits<T>::max_digits10 // decimal digits for lossless binary round-trip (C++11, floats only)

// Type classification
std::numeric_limits<T>::is_signed    // true for signed types
std::numeric_limits<T>::is_integer   // true for integral types
std::numeric_limits<T>::is_exact     // true for integers, false for floats
std::numeric_limits<T>::is_modulo    // true if overflow wraps (always true for unsigned; UB for signed)
std::numeric_limits<T>::is_specialized // false for non-arithmetic types like void*

// Floating-point specifics
std::numeric_limits<T>::epsilon()       // one ulp at 1.0 (difference between 1.0 and next representable value)
std::numeric_limits<T>::round_error()   // max rounding error (≤ 0.5 ulp for IEEE 754)
std::numeric_limits<T>::infinity()      // +∞
std::numeric_limits<T>::quiet_NaN()     // a quiet NaN
std::numeric_limits<T>::denorm_min()    // smallest positive subnormal (C++11)
std::numeric_limits<T>::has_infinity    // true for IEEE 754 types
std::numeric_limits<T>::has_quiet_NaN
std::numeric_limits<T>::is_iec559       // true if IEEE 754 compliant
std::numeric_limits<T>::max_exponent10  // largest power-of-10 exponent for finite values
std::numeric_limits<T>::min_exponent10  // smallest power-of-10 exponent for normalized values

Fixed-width types (<cstdint>, C++11)

cpp
#include <cstdint>

// Exact-width — OPTIONAL: defined only if the platform has a type of exactly N bits with no padding
int8_t   int16_t   int32_t   int64_t
uint8_t  uint16_t  uint32_t  uint64_t

// Least-width — ALWAYS available; at least N bits wide, may be wider
int_least8_t   int_least16_t   int_least32_t   int_least64_t
uint_least8_t  uint_least16_t  uint_least32_t  uint_least64_t

// Fast types — fastest native type that is at least N bits; often 32 or 64 bits on modern hardware
int_fast8_t    int_fast16_t    int_fast32_t    int_fast64_t

// Pointer-sized types (always defined)
intptr_t   // signed, wide enough to hold a void*
uintptr_t  // unsigned equivalent
intmax_t   // widest signed integer type on the platform
uintmax_t

std::numbers (C++20)

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

// Default precision: double
std::numbers::pi        // π   ≈ 3.14159265358979323846
std::numbers::e         // e   ≈ 2.71828182845904523536
std::numbers::phi       // φ   ≈ 1.61803398874989484820 (golden ratio)
std::numbers::sqrt2     // √2  ≈ 1.41421356237309504880
std::numbers::sqrt3     // √3
std::numbers::ln2       // ln(2)
std::numbers::ln10      // ln(10)
std::numbers::log2e     // log₂(e)
std::numbers::log10e    // log₁₀(e)
std::numbers::inv_pi    // 1/π
std::numbers::inv_sqrt3 // 1/√3
std::numbers::egamma    // Euler–Mascheroni constant γ ≈ 0.5772...

// Precision-parameterized form — prefer this in templates
std::numbers::pi_v<float>
std::numbers::pi_v<double>       // same as std::numbers::pi
std::numbers::pi_v<long double>

Examples

Compile-time type assertions (C++11)

cpp
#include <limits>

// Document and enforce platform assumptions at translation time
static_assert(std::numeric_limits<int>::is_signed,    "signed int required");
static_assert(std::numeric_limits<int>::digits >= 31, "32-bit int required");
static_assert(std::numeric_limits<double>::is_iec559, "IEEE 754 double required");
static_assert(sizeof(void*) == 8,                     "64-bit pointer required");

The lowest() vs min() sentinel

cpp
#include <limits>
#include <vector>

// WRONG for floats: numeric_limits<float>::min() ≈ 1.18e-38 (smallest positive *normal*)
// RIGHT: numeric_limits<float>::lowest() ≈ -3.40e+38 (most negative finite value)
// For integers they are the same, but lowest() is correct for both — use it uniformly.

template<typename T>
T range_max(const std::vector<T>& v) {
    T best = std::numeric_limits<T>::lowest();  // safe sentinel for any numeric type
    for (T x : v)
        if (x > best) best = x;
    return best;
}

Lossless floating-point serialization

cpp
#include <limits>
#include <iomanip>
#include <sstream>

// digits10: decimal digits guaranteed to survive a text round-trip
// max_digits10 (C++11): digits needed to reconstruct the exact binary value
//   float:  digits10=6,  max_digits10=9
//   double: digits10=15, max_digits10=17

std::string to_exact(double x) {
    std::ostringstream oss;
    oss << std::setprecision(std::numeric_limits<double>::max_digits10) << x;
    return oss.str();
}

Overflow-safe addition (C++17)

cpp
#include <limits>
#include <optional>

template<typename T>
std::optional<T> checked_add(T a, T b) {
    if constexpr (std::numeric_limits<T>::is_signed) {  // if constexpr: C++17
        if (b > 0 && a > std::numeric_limits<T>::max() - b) return std::nullopt;
        if (b < 0 && a < std::numeric_limits<T>::min() - b) return std::nullopt;
    } else {
        if (a > std::numeric_limits<T>::max() - b) return std::nullopt;
    }
    return a + b;
}

Relative floating-point comparison

cpp
#include <limits>
#include <cmath>

// epsilon() is the ulp at 1.0 — for values of magnitude |x|, the representational
// gap is epsilon() * |x|, not a fixed constant.
bool nearly_equal(double a, double b, int ulps = 8) {
    if (a == b) return true;  // exact match; also handles ±∞
    double diff  = std::abs(a - b);
    double scale = std::max(std::abs(a), std::abs(b));
    // Relative comparison scaled to value magnitude; subnormal floor prevents
    // spurious failures when both values are near zero
    return diff <= std::numeric_limits<double>::epsilon() * scale * ulps
        || diff < std::numeric_limits<double>::min();
}

Best Practices

  • Use std::numeric_limits<T>::max() instead of INT_MAX, LONG_MAX, etc. in all template and generic code. The macros are non-parameterizable and do not compose with type deduction.
  • Always use lowest() as a min-value sentinel, not min(). For floating-point types they differ by ~2 * max().
  • Use max_digits10 (C++11) when emitting floating-point to text storage. digits10 only guarantees recovery of a value semantically equal within its precision; max_digits10 guarantees bit-for-bit reconstruction.
  • In library code targeting exotic embedded targets, prefer int_least32_t over int32_t. The exact-width types are optional in the standard; platforms without a two's-complement 32-bit type with no padding are not required to define int32_t.
  • In std::numbers function templates, write std::numbers::pi_v<T> rather than std::numbers::pi to avoid silently narrowing from double to float.
  • Gate numerical code that depends on IEEE 754 semantics (NaN propagation, signed zero, infinity arithmetic) with static_assert(std::numeric_limits<double>::is_iec559, ...).

Common Pitfalls

min() is not the most-negative float.
std::numeric_limits<float>::min() mirrors FLT_MIN1.18e-38 — the smallest positive normalized value, not the most negative. This trips up generic min-finding code. Use lowest() (C++11) unconditionally.

epsilon() does not scale with magnitude.
epsilon() is one ulp at 1.0. At a value of magnitude x, adjacent representable values are epsilon() * |x| apart. Comparing |a - b| < epsilon() is only meaningful when both operands are close to 1.0; it fails silently for large or small magnitudes.

Signed integer overflow is undefined behavior.
is_modulo is false on virtually all platforms for signed integers. Signed wraparound is not guaranteed and the optimizer is permitted to assume it never happens. Use the checked-add pattern, __builtin_add_overflow (GCC/Clang extension), or the <stdckdint.h> macros (C23 / available as extension).

int32_t may not exist.
The C++ standard makes exact-width types conditional on platform support. Desktop, server, and mobile platforms universally define them, but some DSPs, older mainframes, and 36-bit embedded systems do not. Use int_least32_t for unconditional portability.

is_integer vs is_exact confusion.
is_integer is true only for types the standard calls integral. is_exact is true for integers (exact representation) and false for floats. Both are false for hypothetical rational or fixed-point types, which may have their own specializations.

See Also

  • <climits> / <cfloat> — C-style macros (INT_MAX, DBL_EPSILON); still valid, but non-generic
  • std::is_integral, std::is_floating_point, std::is_arithmetic — type traits that compose with numeric_limits
  • std::bit_cast (C++20) — inspect the bit representation of floating-point values portably
  • __builtin_add_overflow / __builtin_mul_overflow — GCC/Clang builtins for checked arithmetic
  • <cfenv>, std::fegetround — query and control the floating-point rounding mode at runtime