std::numeric_limits
Class template providing compile-time type properties — range, precision, and representation flags — for all arithmetic types, replacing C-style macros.
std::numeric_limits<T>since C++98A class template in <limits> that exposes compile-time properties — range, precision, and representation characteristics — for every fundamental arithmetic type through type-safe, constexpr static members.
Overview
std::numeric_limits<T> replaces the C-style macros from <climits> and <cfloat> (INT_MAX, FLT_EPSILON, etc.) with a uniform template interface. The standard provides specializations for every fundamental arithmetic type: bool, all signed and unsigned integer types, float, double, and long double. Standard library aliases of arithmetic types — std::size_t, std::int32_t, and so on — are introspectable through whatever underlying type they alias on a given platform.
The decisive improvement over macros arrived in C++11: every static member became constexpr, enabling use in array bounds, non-type template arguments, switch case labels, and static_assert without any runtime cost or special dispensation. Before C++11, function members like min() and max() were not integral constant expressions even though they had trivially computable values.
The min() / lowest() distinction
For integer types, min() returns the most negative value, matching INT_MIN. For floating-point types the semantics differ: min() returns the smallest positive normalized value — equivalent to FLT_MIN / DBL_MIN. The most negative representable floating-point value requires lowest(), added in C++11.
#include <limits>
// For integers, min() and lowest() are the same
static_assert(std::numeric_limits<int>::min() ==
std::numeric_limits<int>::lowest()); // C++11
// For double, they are very different
static_assert(std::numeric_limits<double>::min() > 0); // ~2.2e-308
static_assert(std::numeric_limits<double>::lowest() < 0); // ~-1.8e+308Any code that initializes a "running minimum" before a loop must use lowest() for floating-point — using min() produces a sentinel that is immediately replaced by any negative input.
Syntax
#include <limits>
// Static data member
std::numeric_limits<T>::is_signed
// Static member function
std::numeric_limits<T>::max()All members are constexpr since C++11. The primary template with is_specialized == false is provided for non-arithmetic types; querying members on it yields zeroed-out, meaningless values.
Key Members
Range
| Member | Description |
|---|---|
min() | Smallest finite positive value for floats; most negative for integers |
max() | Largest finite value |
lowest() | Most negative finite value — distinct from min() for floats (C++11) |
Precision (floating-point relevant)
| Member | Description |
|---|---|
epsilon() | Difference between 1.0 and the next representable value |
digits | Mantissa bits in radix; for integers, non-sign value bits |
digits10 | Significant base-10 digits that can be represented without change |
max_digits10 | Decimal digits needed for lossless round-trip text conversion (C++11) |
round_error() | Maximum rounding error per ISO/IEC 10967-1 |
Representation flags
| Member | Description |
|---|---|
is_signed | Can represent negative values |
is_integer | Integral type |
is_exact | No rounding error accumulates |
is_iec559 | Conforms to IEEE 754 |
has_infinity | Has a representation for infinity |
has_quiet_NaN | Has a quiet (non-trapping) NaN |
is_specialized | A real specialization exists for this type |
Examples
Overflow-safe arithmetic
#include <limits>
#include <stdexcept>
template <typename T>
T safe_add(T a, T b) {
static_assert(std::numeric_limits<T>::is_integer); // C++11
if (b > 0 && a > std::numeric_limits<T>::max() - b)
throw std::overflow_error{"addition overflow"};
if (b < 0 && a < std::numeric_limits<T>::min() - b)
throw std::overflow_error{"addition underflow"};
return a + b;
}Signed integer overflow is undefined behaviour in C++. Checking against max() / min() before the operation is one of the few standard-compliant approaches that works across all optimization levels.
Generic extremum tracking
#include <limits>
#include <span>
template <typename T>
std::pair<T, T> minmax_span(std::span<const T> data) {
T lo = std::numeric_limits<T>::max();
T hi = std::numeric_limits<T>::lowest(); // C++11 — correct for floats
for (T x : data) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
return {lo, hi};
}Machine epsilon and ULP-based comparison
#include <limits>
#include <cmath>
template <typename T>
bool nearly_equal(T a, T b, int ulps = 4) {
static_assert(std::numeric_limits<T>::is_iec559, "IEEE 754 required");
if (std::isinf(a) || std::isinf(b)) return a == b;
if (std::isnan(a) || std::isnan(b)) return false;
const T eps = std::numeric_limits<T>::epsilon();
const T scale = std::abs(a) + std::abs(b);
// Handle values near zero where relative epsilon breaks down
return std::abs(a - b) <= eps * scale * ulps
|| std::abs(a - b) < std::numeric_limits<T>::min(); // subnormal guard
}Compile-time sizing and static_assert
#include <limits>
#include <array>
// Fully constexpr since C++11 — valid as non-type template argument
constexpr std::size_t byte_table_size =
static_cast<std::size_t>(std::numeric_limits<unsigned char>::max()) + 1; // 256
std::array<int, byte_table_size> lookup{};
// Verify assumptions about the platform's integer layout
static_assert(std::numeric_limits<int>::digits == 31,
"expected 32-bit two's-complement int");Round-trip floating-point serialization
#include <limits>
#include <sstream>
#include <iomanip>
template <typename T>
std::string to_roundtrip_string(T value) {
static_assert(!std::numeric_limits<T>::is_integer);
std::ostringstream oss;
// max_digits10 (C++11) guarantees the value survives a parse/print cycle
oss << std::setprecision(std::numeric_limits<T>::max_digits10)
<< std::scientific << value;
return oss.str();
}Introspecting special values
#include <limits>
#include <cmath>
void special_values_demo() {
using L = std::numeric_limits<double>;
static_assert(L::has_infinity); // true on IEC 559 platforms
static_assert(L::has_quiet_NaN);
const double inf = L::infinity();
const double nan = L::quiet_NaN();
// NaN comparisons always produce false — IEEE 754 requirement
// std::isnan() is the correct test
bool is_nan = std::isnan(nan); // true
bool nan_eq = (nan == nan); // false — not a bug
// Arithmetic with infinity follows IEEE rules
bool inf_check = std::isinf(inf + 1.0); // true
}Best Practices
Prefer numeric_limits over C macros in all new code. INT_MAX, DBL_EPSILON, and their siblings are not type-parameterizable and cannot appear in generic templates. std::numeric_limits<T> scales uniformly across any arithmetic type, including user-defined types that provide their own specialization.
Use lowest() for negative floating-point sentinels. This is the single most common numeric_limits bug. For any floating-point type, min() returns a small positive number. lowest() is the value you want when initializing a variable that will hold the maximum observed value in a range scan.
Use max_digits10 when serializing floats to text. digits10 tells you how many significant decimal digits a type can read without loss; max_digits10 (C++11) tells you how many decimal digits you must write to guarantee a lossless round-trip. Always use max_digits10 with std::setprecision when persisting floating-point values as strings.
Guard IEEE 754 assumptions with is_iec559. Infinity arithmetic, NaN propagation, and the specific definition of epsilon() are only reliable when is_iec559 is true. Most x86/x64 and ARM platforms satisfy this for float and double, but making the assumption explicit with static_assert(std::numeric_limits<T>::is_iec559) documents the dependency and catches exotic targets early.
Verify is_specialized before querying an unknown type. The primary template sets is_specialized = false and zeroes all other members. Generic infrastructure that accepts arbitrary T should check is_specialized (or use the std::is_arithmetic type trait) before relying on any query result.
Common Pitfalls
min() for floats is not the most negative value. std::numeric_limits<double>::min() ≈ 2.2×10⁻³⁰⁸. This matches DBL_MIN exactly, and surprises developers expecting the parallel with std::numeric_limits<int>::min(). C++11's lowest() fills the gap.
NaN is not equal to itself. std::numeric_limits<double>::quiet_NaN() == std::numeric_limits<double>::quiet_NaN() is false. This is correct per IEEE 754 but means NaN values break sorted containers, hash maps with equality predicates, and any algorithm relying on reflexive equality. Use std::isnan() from <cmath> to detect NaN explicitly.
has_denorm and denorm_min() are deprecated in C++23. Code checking std::numeric_limits<T>::has_denorm or the float_denorm_style enumeration should be reviewed; these are scheduled for removal and compiler warnings may appear under -std=c++23.
Function members were not constexpr before C++11. Code that must compile in C++03 cannot use numeric_limits<T>::max() as a compile-time constant. The C macros (INT_MAX, etc.) remain the only compile-time option under that standard.
Deep Dive: min() vs lowest() for Floating-Point
The single most common std::numeric_limits mistake is using min() as the most-negative float. The confusion is understandable: for every integer type, min() returns the most-negative representable value (e.g., -2147483648 for int). For floating-point types, the standard explicitly broke this parallel because IEEE 754 already has a value — DBL_MIN — that describes the smallest positive normalized number. lowest() was added in C++11 to fill the gap.
Concrete values for IEEE 754 double:
| Member | Value | Hex exponent pattern | Equivalent C macro |
|---|---|---|---|
min() | ~2.2250738585072014×10⁻³⁰⁸ | 0x0010000000000000 | DBL_MIN |
max() | ~1.7976931348623157×10³⁰⁸ | 0x7FEFFFFFFFFFFFFF | DBL_MAX |
lowest() | ~-1.7976931348623157×10³⁰⁸ | 0xFFEFFFFFFFFFFFFF | -DBL_MAX |
The bug this causes in practice is subtle. A loop that initializes a "running maximum" sentinel with min() instead of lowest() will silently fail when all input values are negative — the sentinel is already larger than every negative number in the input:
#include <limits>
#include <vector>
// BUGGY: sentinel is ~2.2e-308, larger than all negative inputs
double find_max_buggy(const std::vector<double>& v) {
double result = std::numeric_limits<double>::min(); // WRONG for float
for (double x : v) if (x > result) result = x;
return result; // returns ~2.2e-308 even if all inputs are -1.0
}
// CORRECT: lowest() is the most negative finite double (~-1.8e308)
double find_max_correct(const std::vector<double>& v) {
double result = std::numeric_limits<double>::lowest(); // C++11
for (double x : v) if (x > result) result = x;
return result;
}The word "lowest" was chosen deliberately over "minimum" to avoid adding a third overloaded meaning to min. The same distinction applies to float and long double.
Subnormal numbers and denorm_min()
Below min() lies the subnormal (denormalized) range. IEEE 754 doubles can represent values as small as ~5×10⁻³²⁴ using subnormals, at the cost of reduced precision. denorm_min() returns the smallest positive subnormal value. Check has_denorm (deprecated C++23) or simply is_iec559 before relying on subnormals:
#include <limits>
using L = std::numeric_limits<double>;
// Smallest positive normalized: ~2.225e-308 (2^-1022)
constexpr double norm_min = L::min();
// Smallest positive subnormal: ~5.000e-324 (2^-1074)
constexpr double subnorm_min = L::denorm_min(); // C++98, deprecated C++23
// The gap between them:
static_assert(subnorm_min < norm_min); // always true on IEEE 754denorm_min() and the float_denorm_style enum (has_denorm, has_denorm_loss) are deprecated in C++23 and scheduled for removal. On all IEEE 754 platforms has_denorm is std::denorm_present; guard assumptions with is_iec559 instead.
Floating-Point Special Values: Infinity and NaN
IEEE 754 reserves specific bit patterns for three non-finite states: positive infinity, negative infinity, and NaN (Not a Number). std::numeric_limits exposes these through member functions that are only meaningful when the corresponding flag is true.
has_infinity and infinity()
#include <limits>
#include <cmath>
static_assert(std::numeric_limits<double>::has_infinity);
static_assert(std::numeric_limits<float>::has_infinity);
// Integer types never have infinity:
static_assert(!std::numeric_limits<int>::has_infinity);
constexpr double pos_inf = std::numeric_limits<double>::infinity();
constexpr double neg_inf = -std::numeric_limits<double>::infinity();
// Infinity arithmetic follows IEEE 754 rules:
// inf + finite = inf
// inf - inf = NaN
// 1.0 / 0.0 = inf (on IEEE 754; UB in C++ before -ffast-math is disabled)
static_assert(std::isinf(pos_inf));
static_assert(!std::isnan(pos_inf));infinity() returns positive infinity. Negate it for negative infinity. Infinity is the natural sentinel for algorithms that start with no bound: a range-minimum scan over an empty range should return +inf, not max(), because the mathematical infimum of the empty set is positive infinity.
has_quiet_NaN, has_signaling_NaN, and their values
#include <limits>
#include <cmath>
using L = std::numeric_limits<double>;
static_assert(L::has_quiet_NaN);
// has_signaling_NaN is true on most IEEE 754 implementations but
// signaling NaNs are difficult to use portably — hardware often
// immediately quiets them or raises SIGFPE.
const double qnan = L::quiet_NaN();
const double snan = L::signaling_NaN(); // use with extreme caution
// The defining property of NaN: it compares unequal to everything, including itself.
bool self_eq = (qnan == qnan); // false — this is correct, not a bug
bool self_ne = (qnan != qnan); // true
bool detected = std::isnan(qnan); // true — the only reliable test
// NaN propagates through arithmetic:
double x = qnan + 1.0; // NaN
double y = qnan * 0.0; // NaN (not 0)
double z = qnan < 0.0 ? -1.0 : 1.0; // 1.0 — comparisons with NaN are falseQuiet NaN propagates silently through arithmetic. Signaling NaN raises a floating-point exception (SIGFPE or similar) when touched on platforms that enable trapping — it is mainly useful in debugging to detect propagation of uninitialized values. In production code, use quiet NaN as a sentinel for "no value" in floating-point streams.
is_iec559: the IEEE 754 compliance flag
is_iec559 is the key that unlocks all the above. When is_iec559 is true, all of the following hold: infinity and NaN are present, epsilon() has the exact meaning defined by IEEE 754, arithmetic follows the standard rounding rules, and the bit representation of floats is standardized. Guard every IEEE-specific assumption with it:
#include <limits>
template <typename T>
T ieee_safe_divide(T a, T b) {
static_assert(std::numeric_limits<T>::is_iec559,
"IEEE 754 division semantics required");
// On IEEE 754, a/0.0 = +-inf, 0.0/0.0 = NaN — no trap, no UB.
// Without is_iec559, division by zero is implementation-defined.
return a / b;
}On x86-64 with GCC/Clang/MSVC and -fno-fast-math (the default), is_iec559 is true for float and double. With -ffast-math, the compiler is permitted to violate IEEE 754 semantics and is_iec559 may remain true in the traits yet the behavior no longer conforms — a known inconsistency between the flag and actual runtime behavior.
epsilon() and Floating-Point Comparison
epsilon() returns the machine epsilon: the difference between 1.0 and the smallest floating-point value greater than 1.0. In IEEE 754 terms it is 2^(1 − significand_bits):
| Type | epsilon() | Exact power |
|---|---|---|
float | ~1.192×10⁻⁷ | 2⁻²³ |
double | ~2.220×10⁻¹⁶ | 2⁻⁵² |
long double (80-bit x87) | ~1.084×10⁻¹⁹ | 2⁻⁶³ |
The wrong way to use epsilon
// WRONG: only works when a and b are both near 1.0
bool approx_eq_wrong(double a, double b) {
return std::abs(a - b) < std::numeric_limits<double>::epsilon();
}
// When a = 1e10 and b = 1e10 + 1.0, the difference is 1.0, far larger
// than epsilon (~2.2e-16), so this returns false even though the values
// differ by only 1 ULP at that scale.
//
// When a = 1e-20 and b = 2e-20, the difference is 1e-20 which is smaller
// than epsilon, so this returns true even though they differ by 100%.The right way: relative epsilon
#include <limits>
#include <cmath>
// Relative epsilon comparison — correct for values away from zero.
template <typename T>
bool nearly_equal_rel(T a, T b, int ulps = 4) {
static_assert(std::numeric_limits<T>::is_iec559);
if (a == b) return true; // handles infinities
if (std::isnan(a) || std::isnan(b)) return false;
const T eps = std::numeric_limits<T>::epsilon();
const T scale = std::max(std::abs(a), std::abs(b));
return std::abs(a - b) <= eps * scale * static_cast<T>(ulps);
}
// For values near zero, fall back to an absolute tolerance:
template <typename T>
bool nearly_equal(T a, T b,
T abs_tol = std::numeric_limits<T>::min(),
int ulps = 4)
{
static_assert(std::numeric_limits<T>::is_iec559);
if (std::isnan(a) || std::isnan(b)) return false;
if (a == b) return true;
const T diff = std::abs(a - b);
const T scale = std::max(std::abs(a), std::abs(b));
const T eps = std::numeric_limits<T>::epsilon();
return diff <= abs_tol // near-zero guard
|| diff <= eps * scale * static_cast<T>(ulps); // relative check
}The ulps multiplier accounts for accumulated rounding across multiple floating-point operations. A single IEEE 754 operation introduces at most 0.5 ULP of error; after k operations the accumulated error is at most roughly k * 0.5 ULPs. Using ulps = 4 gives a safe margin for expressions with a handful of operations.
Integer Limits and Overflow Detection
For integer types, numeric_limits provides the same information as the C macros but in a type-parameterizable, template-friendly form:
| C macro | std::numeric_limits<T> equivalent | Type |
|---|---|---|
CHAR_MIN / CHAR_MAX | min() / max() | char |
SHRT_MIN / SHRT_MAX | min() / max() | short |
INT_MIN / INT_MAX | min() / max() | int |
LONG_MIN / LONG_MAX | min() / max() | long |
LLONG_MIN / LLONG_MAX | min() / max() | long long |
UINT_MAX | max() | unsigned int |
Structural members: is_signed, digits, digits10
#include <limits>
using L = std::numeric_limits<int>;
// is_signed: true for signed types
static_assert(L::is_signed);
static_assert(!std::numeric_limits<unsigned int>::is_signed);
// digits: the number of non-sign bits in the representation.
// For a 32-bit two's-complement int this is 31.
// For an unsigned 32-bit int this is 32.
static_assert(L::digits == 31);
static_assert(std::numeric_limits<unsigned int>::digits == 32);
// digits10: the number of decimal digits that can always be represented
// without change — i.e., floor(digits * log10(2)).
// For int (31 bits): floor(31 * 0.30103) = floor(9.33) = 9
static_assert(L::digits10 == 9);digits is the count of value bits — excluding the sign bit for signed types. digits10 is the number of decimal digits that fit without loss. These are the correct values to use when sizing output buffers or implementing arbitrary-precision conversions.
Overflow detection idiom
#include <limits>
#include <stdexcept>
#include <type_traits>
// Detect signed multiplication overflow before it happens (UB otherwise).
template <typename T>
T safe_mul(T a, T b) {
static_assert(std::numeric_limits<T>::is_integer);
static_assert(std::numeric_limits<T>::is_signed);
if (a != 0 && b != 0) {
const T max = std::numeric_limits<T>::max();
const T min = std::numeric_limits<T>::min();
if ((a > 0 && b > 0 && a > max / b) ||
(a < 0 && b < 0 && a < max / b) ||
(a > 0 && b < 0 && b < min / a) ||
(a < 0 && b > 0 && a < min / b))
{
throw std::overflow_error{"multiplication overflow"};
}
}
return a * b;
}Specializing std::numeric_limits for Custom Types
The standard permits and encourages user specializations of std::numeric_limits for user-defined arithmetic types. This is how a fixed-point number class, a big-integer library, or a saturating integer can participate in generic numeric algorithms that query limits.
What to provide
The standard's primary template sets is_specialized = false and zeroes all members. A valid specialization must at minimum set is_specialized = true and provide meaningful values for the members that the type actually supports. The pattern:
#include <limits>
// A simple Q15 fixed-point type: 1 sign bit + 15 fractional bits,
// representing values in [-1.0, 1.0) with resolution 2^-15.
struct Q15 {
int16_t raw;
static constexpr Q15 from_raw(int16_t r) noexcept { return Q15{r}; }
constexpr double to_double() const noexcept { return raw / 32768.0; }
};
namespace std {
template <>
struct numeric_limits<Q15> {
// Required: announce that this specialization is real
static constexpr bool is_specialized = true;
// Integer-like structure
static constexpr bool is_signed = true;
static constexpr bool is_integer = false; // fractional values
static constexpr bool is_exact = false; // rounding occurs
static constexpr bool is_modulo = false;
// No IEEE 754 special values
static constexpr bool is_iec559 = false;
static constexpr bool has_infinity = false;
static constexpr bool has_quiet_NaN = false;
static constexpr bool has_signaling_NaN = false;
// Bit structure: 1 sign bit + 15 value bits
static constexpr int digits = 15; // non-sign bits
static constexpr int digits10 = 4; // floor(15 * log10(2)) = 4
static constexpr int radix = 2;
// Range: raw value spans [-32768, 32767] / 32768
static constexpr Q15 min() noexcept { return Q15::from_raw(-32767); }
static constexpr Q15 max() noexcept { return Q15::from_raw( 32767); }
static constexpr Q15 lowest() noexcept { return Q15::from_raw(-32767); }
// Resolution: 2^-15
static constexpr Q15 epsilon() noexcept { return Q15::from_raw(1); }
static constexpr Q15 round_error() noexcept { return Q15::from_raw(1); }
// Unused special-value functions still must be provided
static constexpr Q15 infinity() noexcept { return Q15{}; }
static constexpr Q15 quiet_NaN() noexcept { return Q15{}; }
static constexpr Q15 signaling_NaN() noexcept { return Q15{}; }
static constexpr Q15 denorm_min() noexcept { return Q15{}; }
};
}With this specialization, generic algorithms using std::numeric_limits<T>::max() or std::numeric_limits<T>::lowest() as sentinels — including std::minmax_element analogues, serialization helpers, or the overflow-detection safe_add pattern — will work correctly with Q15.
What is mandatory vs. optional
The standard does not list specific members as "mandatory" in a hard sense — the primary template provides defaults for everything. What matters in practice:
is_specialized = true— without this, code checkingif constexpr (std::numeric_limits<T>::is_specialized)will skip your type entirely.min(),max(),lowest()— almost all generic range-scanning code relies on these.is_integer,is_signed— used instatic_assertguards in many numeric utilities.epsilon()— needed by floating-point-style comparison infrastructure if your type supports approximate equality.- All
static constexprdata members should be initialized, and all static member functions should be provided even if they return a zeroed value, because the primary template makes them available and unchecked calls by generic code will not compile without them.
C++23 Fixed-Width Float Types and numeric_limits
C++23 (via P1467R9) added five optional fixed-width floating-point types in <stdfloat>: std::float16_t, std::bfloat16_t, std::float32_t, std::float64_t, and std::float128_t. Each has a full std::numeric_limits specialization when the type is available. Guard all use with the corresponding availability macro.
Key numeric_limits values for the C++23 float types
| Member | float16_t (binary16) | bfloat16_t | float32_t (binary32) | float64_t (binary64) |
|---|---|---|---|---|
digits | 11 | 8 | 24 | 53 |
digits10 | 3 | 2 | 6 | 15 |
epsilon() | ~9.766×10⁻⁴ (2⁻¹⁰) | ~7.813×10⁻³ (2⁻⁷) | ~1.192×10⁻⁷ (2⁻²³) | ~2.220×10⁻¹⁶ (2⁻⁵²) |
max() | 65504 | ~3.390×10³⁸ | ~3.403×10³⁸ | ~1.798×10³⁰⁸ |
min() (pos. normal) | ~6.104×10⁻⁵ | ~1.175×10⁻³⁸ | ~1.175×10⁻³⁸ | ~2.225×10⁻³⁰⁸ |
is_iec559 | true | false | true | true |
The most important cell in that table is bfloat16_t::is_iec559 == false. bfloat16_t was designed by Google Brain as a truncated float32_t (same 8-bit exponent, only 7 explicit mantissa bits instead of 23). It is not part of the IEEE 754-2019 standard. It does support infinity and NaN with the same encoding as float32_t, so has_infinity and has_quiet_NaN are both true for bfloat16_t — but is_iec559 is false because the format is not IEEE-standardized.
#include <stdfloat> // C++23
#include <limits>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_BFLOAT16_T__)
// float16_t: IEEE 754 binary16 — tight precision, limited dynamic range
static_assert(std::numeric_limits<std::float16_t>::is_iec559 == true);
static_assert(std::numeric_limits<std::float16_t>::digits == 11);
static_assert(std::numeric_limits<std::float16_t>::max_exponent == 16);
// bfloat16_t: not IEEE 754, but has NaN and infinity
static_assert(std::numeric_limits<std::bfloat16_t>::is_iec559 == false);
static_assert(std::numeric_limits<std::bfloat16_t>::has_infinity == true);
static_assert(std::numeric_limits<std::bfloat16_t>::has_quiet_NaN == true);
static_assert(std::numeric_limits<std::bfloat16_t>::digits == 8);
// bfloat16 shares exponent range with float32
static_assert(std::numeric_limits<std::bfloat16_t>::max_exponent ==
std::numeric_limits<std::float32_t>::max_exponent); // both 128
// float16 max is only 65504 — easy to overflow from float32 computation
constexpr auto f16_max = std::numeric_limits<std::float16_t>::max(); // 65504
constexpr auto f32_max = std::numeric_limits<std::float32_t>::max(); // ~3.4e38
#endifPractical pattern: precision-aware dispatch
A generic numeric kernel can select its working precision at compile time based on epsilon():
#include <stdfloat>
#include <limits>
#include <type_traits>
// Select a wider accumulator type when T has insufficient precision.
template <typename T>
struct wider_float {
// Default: accumulate in double
using type = double;
};
#if defined(__STDCPP_FLOAT16_T__)
template <>
struct wider_float<std::float16_t> {
using type = std::float32_t; // widen f16 -> f32 for accumulation
};
#endif
#if defined(__STDCPP_BFLOAT16_T__)
template <>
struct wider_float<std::bfloat16_t> {
using type = std::float32_t; // widen bf16 -> f32 for accumulation
};
#endif
template <typename T>
typename wider_float<T>::type kahan_sum(const T* data, std::size_t n) {
using Acc = typename wider_float<T>::type;
Acc sum = Acc{0};
Acc c = Acc{0};
for (std::size_t i = 0; i < n; ++i) {
Acc y = static_cast<Acc>(data[i]) - c;
Acc t = sum + y;
c = (t - sum) - y;
sum = t;
}
return sum;
}See Also
- Numeric algorithms —
std::accumulate,std::partial_sum, and related algorithms where limit values serve as arithmetic identities