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++98A 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
#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 valuesFixed-width types (<cstdint>, C++11)
#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_tstd::numbers (C++20)
#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)
#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
#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
#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)
#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
#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 ofINT_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, notmin(). For floating-point types they differ by ~2 * max(). - Use
max_digits10(C++11) when emitting floating-point to text storage.digits10only guarantees recovery of a value semantically equal within its precision;max_digits10guarantees bit-for-bit reconstruction. - In library code targeting exotic embedded targets, prefer
int_least32_toverint32_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 defineint32_t. - In
std::numbersfunction templates, writestd::numbers::pi_v<T>rather thanstd::numbers::pito avoid silently narrowing fromdoubletofloat. - 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_MIN ≈ 1.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-genericstd::is_integral,std::is_floating_point,std::is_arithmetic— type traits that compose withnumeric_limitsstd::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