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, std::numbers math constants, epsilon/ULP arithmetic, and safe overflow detection 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();
}

C Macros vs std::numeric_limits — Side-by-Side

Before C++11, numeric bounds were accessed through C macros from <climits> and <cfloat>. These macros still compile in C++ but cannot be used in template parameters or with type deduction. The table below shows the direct equivalents; the numeric_limits forms are strictly preferable in any context that involves templates or constexpr.

C macrostd::numeric_limits<T> equivalentNotes
INT_MINnumeric_limits<int>::min()Same value; but min() on floats is not the most negative
INT_MAXnumeric_limits<int>::max()Identical semantics
UINT_MAXnumeric_limits<unsigned int>::max()
LONG_MAXnumeric_limits<long>::max()
LLONG_MAXnumeric_limits<long long>::max()
FLT_MINnumeric_limits<float>::min()Smallest positive normal, not most negative — see below
FLT_MAXnumeric_limits<float>::max()
FLT_EPSILONnumeric_limits<float>::epsilon()
DBL_EPSILONnumeric_limits<double>::epsilon()
DBL_MAXnumeric_limits<double>::max()
LDBL_MAXnumeric_limits<long double>::max()
(no equivalent)numeric_limits<float>::lowest()Most negative finite float ≈ -3.40e+38; C++11 only
(no equivalent)numeric_limits<double>::lowest()Most negative finite double ≈ -1.80e+308

The min() / lowest() trap is the single most common error when porting macro-based code to templates. FLT_MIN and numeric_limits<float>::min() both return the smallest positive normalized value (~1.18×10⁻³⁸). There is no C macro for the most negative float; lowest() was added in C++11 precisely to fill this gap. Generic code that initializes a running minimum with numeric_limits<T>::min() will produce incorrect results for any floating-point type T. Use lowest() unconditionally in sentinel initialization.

cpp
#include <limits>
#include <cfloat>   // C macros for comparison

// These pairs are equivalent:
static_assert(std::numeric_limits<int>::max()        == INT_MAX);   // C++11
static_assert(std::numeric_limits<double>::epsilon() == DBL_EPSILON);

// But there is NO macro for lowest():
float most_negative = std::numeric_limits<float>::lowest();  // ≈ -3.40e+38  // C++11
// FLT_MIN ≈ 1.18e-38  <-- smallest positive normal, not most negative!

// In generic code the macro forms simply cannot be used:
template<typename T>
T safe_min_sentinel() {
    return std::numeric_limits<T>::lowest();  // works for int, float, double, long double...
    // return INT_MIN;  // ERROR: does not compile with T=float or T=double
}

Floating-Point Epsilon and ULP Arithmetic

What epsilon() actually measures

epsilon() returns the difference between 1.0 and the next representable value above 1.0 for type T. For IEEE 754 double this is 2⁻⁵² ≈ 2.22×10⁻¹⁶. This quantity is often called machine epsilon or one ULP at 1.0 (Unit in the Last Place at 1.0).

The key insight is that the spacing between adjacent representable values is not constant — it scales with magnitude. Near the value x, adjacent floats are roughly epsilon() * |x| apart. Comparing |a - b| < epsilon() is only reliable when both a and b are close to 1.0. For values of order 1000, the representable spacing is about 1000 times larger than epsilon().

cpp
#include <limits>
#include <cmath>
#include <cassert>

// Relative epsilon comparison — correct for arbitrary magnitudes
template<typename T>
bool approx_equal(T a, T b, T rel_tol = T{4} * std::numeric_limits<T>::epsilon()) {
    if (a == b) return true;                          // handles ±∞ exactly
    T diff  = std::abs(a - b);
    T scale = std::max(std::abs(a), std::abs(b));
    return diff <= rel_tol * scale;                   // relative test scales with magnitude
}

// Works correctly across magnitudes:
assert( approx_equal(1.0,      1.0 + 3e-16));         // near 1.0  -- C++11
assert( approx_equal(1.0e10,   1.0e10 + 3e-6));       // near 1e10 -- spacing ~2e-6 there
assert(!approx_equal(1.0e10,   1.0e10 + 1.0));        // 1.0 is > 4 ULPs at this scale

Stepping by ULPs with std::nextafter

When you need to step exactly one representable value at a time — for example, to bracket a root or to test boundary conditions — use std::nextafter from <cmath> (C++11). It returns the next representable value in the direction of the second argument.

cpp
#include <cmath>    // std::nextafter  // C++11
#include <limits>
#include <iostream>

double x = 1.0;
double next = std::nextafter(x, std::numeric_limits<double>::infinity());  // one ULP above 1.0
double prev = std::nextafter(x, -std::numeric_limits<double>::infinity()); // one ULP below 1.0

std::cout << (next - x);  // prints 2.22045e-16, which equals epsilon()
std::cout << (x - prev);  // prints 1.11022e-16, which is epsilon()/2 (asymmetric below 1.0)

// Count ULPs between two values (works for small differences):
// Useful for validating numerical algorithms against a tolerance in ULPs
double a = 1.0;
double b = 1.0 + std::numeric_limits<double>::epsilon() * 3;
double step = a;
int ulp_count = 0;
while (step < b) { step = std::nextafter(step, b); ++ulp_count; }
// ulp_count == 3

Subnormal numbers and denorm_min()

When a floating-point result is too small for a normalized representation, IEEE 754 produces a subnormal (or denormalized) value with reduced precision. denorm_min() is the smallest positive subnormal — for double this is approximately 5×10⁻³²⁴.

cpp
#include <limits>

// Subnormal territory: values below min() but above zero
static_assert(std::numeric_limits<double>::has_denorm != std::denorm_absent); // C++11

double smallest_normal    = std::numeric_limits<double>::min();       // ≈ 2.23e-308
double smallest_subnormal = std::numeric_limits<double>::denorm_min(); // ≈ 4.94e-324

// Comparing near-zero floats with a fixed epsilon() fails in subnormal territory.
// The subnormal floor guards against this:
bool safe_nearly_zero(double x) {
    return std::abs(x) < std::numeric_limits<double>::min();  // below normal range
}

Generic Numeric Algorithms with numeric_limits

numeric_limits enables writing algorithm templates that adapt correctly to both integer and floating-point types without type-specific branches.

Safe sentinel initialization

cpp
#include <limits>
#include <vector>
#include <algorithm>

// Generic min and max that work for int, unsigned, float, double, long double...
template<typename T>
std::pair<T, T> minmax_of(const std::vector<T>& v) {
    T lo = std::numeric_limits<T>::max();      // start above everything
    T hi = std::numeric_limits<T>::lowest();   // start below everything  // C++11
    for (T x : v) {
        if (x < lo) lo = x;
        if (x > hi) hi = x;
    }
    return {lo, hi};
}

Type-safe clamp

cpp
#include <limits>

// Clamp a value of type U into the range representable by T.
// Useful when reading user input or external data into a narrower type.
template<typename T, typename U>
T clamp_to(U value) {
    if (value > static_cast<U>(std::numeric_limits<T>::max()))
        return std::numeric_limits<T>::max();
    if (value < static_cast<U>(std::numeric_limits<T>::lowest()))   // C++11
        return std::numeric_limits<T>::lowest();
    return static_cast<T>(value);
}

// Example: safely narrow a 64-bit sensor reading into int16_t
int64_t raw = 70000;
int16_t clamped = clamp_to<int16_t>(raw);  // returns 32767 (INT16_MAX)

Overflow detection using max()

cpp
#include <limits>
#include <stdexcept>

// safe_add: returns a + b or throws on overflow.
// Works for any signed or unsigned integer type.  // C++17
template<typename T>
T safe_add(T a, T b) {
    static_assert(std::numeric_limits<T>::is_integer, "safe_add requires an integer type");

    if constexpr (std::numeric_limits<T>::is_signed) {
        // Positive overflow: a + b > max()  =>  b > max() - a
        if (b > T{0} && a > std::numeric_limits<T>::max() - b)
            throw std::overflow_error("safe_add: positive overflow");
        // Negative overflow: a + b < lowest()  =>  b < lowest() - a
        if (b < T{0} && a < std::numeric_limits<T>::lowest() - b)
            throw std::overflow_error("safe_add: negative overflow");
    } else {
        // Unsigned: only one direction possible
        if (a > std::numeric_limits<T>::max() - b)
            throw std::overflow_error("safe_add: unsigned overflow");
    }
    return a + b;
}

// Similarly for multiplication — check before the multiply, not after:
template<typename T>
T safe_mul(T a, T b) {
    static_assert(std::numeric_limits<T>::is_integer, "safe_mul requires an integer type");
    if (b != T{0} && a > std::numeric_limits<T>::max() / b)
        throw std::overflow_error("safe_mul: overflow");
    return a * b;
}

Fixed-Width Integers (<cstdint>) in Depth

<cstdint> provides three families of integer types, each serving a different portability/performance trade-off. All three families are available via std::numeric_limits because the aliases ultimately resolve to one of the fundamental integer types.

The three families

cpp
#include <cstdint>   // C++11
#include <limits>

// 1. Exact-width: int8_t through int64_t
//    - OPTIONAL — not guaranteed on platforms that lack a matching native type
//    - Desktop/server/mobile: always present (two's complement since C++20)
//    - Use when the bit width is contractual (binary serialization, hardware registers)
static_assert(sizeof(int32_t) == 4);
static_assert(std::numeric_limits<int32_t>::digits == 31);  // 31 value bits + 1 sign bit

// 2. Least-width: int_least8_t through int_least64_t
//    - ALWAYS defined; guaranteed at least N bits wide
//    - May be wider (e.g., int_least8_t could be 32 bits on a DSP with no 8-bit type)
//    - Use for unconditional portability where exact width is not required
int_least32_t counter = 0;  // safe on every conforming C++ platform

// 3. Fast types: int_fast8_t through int_fast64_t
//    - Fastest native type that is at least N bits
//    - On x86-64, int_fast8_t and int_fast16_t are typically 64-bit (avoids widening)
//    - Use for loop counters and temporaries where maximum throughput matters
int_fast16_t loop_var = 0;  // may be 64-bit on x86-64

// Pointer-sized types
intptr_t  ptr_as_int = reinterpret_cast<intptr_t>(nullptr);   // store a pointer as integer
uintptr_t ptr_bits   = reinterpret_cast<uintptr_t>(nullptr);
intmax_t  biggest    = std::numeric_limits<intmax_t>::max();   // widest type the platform has

numeric_limits works through typedefs

Because int32_t is just a typedef for some fundamental type, std::numeric_limits<int32_t> works without any additional specialization:

cpp
#include <cstdint>
#include <limits>

// All of these resolve to the underlying fundamental type's specialization:
constexpr int32_t  i32_max = std::numeric_limits<int32_t>::max();   // 2147483647   // C++11
constexpr uint64_t u64_max = std::numeric_limits<uint64_t>::max();  // 18446744073709551615
constexpr int8_t   i8_min  = std::numeric_limits<int8_t>::lowest(); // -128

// Useful for format strings and serialization:
static_assert(std::numeric_limits<int32_t>::digits == 31,  "int32_t must be 32-bit");
static_assert(std::numeric_limits<uint8_t>::is_modulo,     "unsigned types wrap");
static_assert(!std::numeric_limits<int64_t>::is_modulo,    "signed types do not wrap (UB)");

Choosing the right family

  • Use int32_t / uint64_t when the exact bit width is part of the interface contract: network protocols, file formats, hardware register maps, or interoperating with C code that uses the same types.
  • Use int_least32_t in headers that must compile on exotic embedded targets (DSPs, 36-bit systems) where int32_t may not exist.
  • Use int_fast32_t inside performance-critical loops on hot paths where you want the compiler to pick the most efficient width for the current architecture.
  • Use intptr_t only when you genuinely need to do arithmetic on a pointer value; prefer std::uintptr_t for bit manipulation of addresses.

std::numbers Constants (C++20)

<numbers> provides constexpr values for common mathematical constants, correctly rounded for each floating-point precision. Every constant exists in two forms: an unparameterized alias (always double) and a variable template (_v<T>) that supports float, double, and long double.

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

// Unparameterized aliases are always double:
double area = std::numbers::pi * r * r;

// Variable template form — preserves precision, avoids implicit narrowing:
float  area_f = std::numbers::pi_v<float>  * rf * rf;   // no double->float narrowing
double area_d = std::numbers::pi_v<double> * rd * rd;   // same as std::numbers::pi

// Full list of constants:
//  pi        3.14159265358979323846...
//  e         2.71828182845904523536...
//  phi       1.61803398874989484820...   (golden ratio)
//  sqrt2     1.41421356237309504880...
//  sqrt3     1.73205080756887729352...
//  ln2       0.69314718055994530941...
//  ln10      2.30258509299404568401...
//  log2e     1.44269504088896340735...
//  log10e    0.43429448190325182765...
//  inv_pi    0.31830988618379067153...
//  inv_sqrt3 0.57735026918962576450...
//  egamma    0.57721566490153286060...   (Euler-Mascheroni constant)

Best practice: always use pi_v<T> in templates, not pi. When T is float, writing std::numbers::pi silently narrows a double to float at the assignment, which may not be obvious. The _v<T> form carries the full precision of the target type from the start.

cpp
// C++20 — template that needs pi at the right precision:
template<typename T>
T circle_area(T radius) {
    return std::numbers::pi_v<T> * radius * radius;  // correct: no narrowing, no lost bits
}

// Incorrect version — would silently narrow to float when T=float:
// return std::numbers::pi * radius * radius;  // pi is double; radius*radius is float
//                                             // result is float but intermediate pi is double

Custom Type Specialization

You can extend std::numeric_limits to user-defined types by providing an explicit specialization in namespace std. This is one of the few cases where the standard permits specializing std:: templates.

The primary motivation is to make generic numeric algorithms — range finders, serializers, overflow checkers — work transparently with your custom numeric type, just as they do with int and double.

cpp
#include <limits>

// A simple 16-bit fixed-point type with 8 integer bits and 8 fractional bits.
// Stored value = raw / 256.0
struct Fixed16 {
    int16_t raw;

    static constexpr Fixed16 from_raw(int16_t r) noexcept { return Fixed16{r}; }
    static constexpr Fixed16 from_double(double d) noexcept {
        return Fixed16{static_cast<int16_t>(d * 256.0)};
    }
    constexpr double to_double() const noexcept { return raw / 256.0; }
};

namespace std {
    template<>
    struct numeric_limits<Fixed16> {
        // Mandatory: tell generic code this specialization is active
        static constexpr bool is_specialized = true;

        // Classification
        static constexpr bool is_signed  = true;    // can represent negative values
        static constexpr bool is_integer = false;   // not an integer type
        static constexpr bool is_exact   = true;    // every stored bit pattern is exact
        static constexpr bool is_modulo  = false;   // overflow is not defined to wrap

        // Floating-point-style flags (none apply here)
        static constexpr bool is_iec559     = false;
        static constexpr bool has_infinity  = false;
        static constexpr bool has_quiet_NaN = false;

        // Precision: 8 fractional bits => 8 binary digits after the point
        static constexpr int digits   = 15;  // 15 value bits (16 total, 1 sign)
        static constexpr int digits10 = 2;   // roughly log10(2^8) ≈ 2.4 decimal digits

        // Range and sentinels                                              // C++11
        static constexpr Fixed16 min()    noexcept { return Fixed16::from_raw(1);         } // smallest positive
        static constexpr Fixed16 max()    noexcept { return Fixed16::from_raw(32767);     } // largest value
        static constexpr Fixed16 lowest() noexcept { return Fixed16::from_raw(-32768);    } // most negative
        static constexpr Fixed16 epsilon() noexcept { return Fixed16::from_raw(1);        } // one LSB
    };
}  // namespace std

// Now generic algorithms work correctly with Fixed16:
#include <vector>

std::vector<Fixed16> readings = { Fixed16{100}, Fixed16{-50}, Fixed16{200} };
Fixed16 hi = std::numeric_limits<Fixed16>::lowest();
for (auto x : readings)
    if (x.raw > hi.raw) hi = x;
// hi.raw == 200

The critical members to provide for a usable specialization are is_specialized, is_signed, is_integer, min(), max(), and lowest(). The rest can remain at their default values (false / 0) as long as they accurately reflect the type.


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, ...).
  • Use std::nextafter for ULP-stepping instead of manual epsilon arithmetic. It is correct across subnormal boundaries and does not require knowing the exponent of the current value.

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.

Never use std::numbers::pi where float precision is needed.
std::numbers::pi is double. Assigning it to a float or using it in a float expression silently narrows to 24 significant bits. Use std::numbers::pi_v<float> explicitly.

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
  • std::nextafter, std::nexttoward (<cmath>, C++11) — step by one ULP in a given direction