C++ std::numeric_limits: Type Limits, NaN and Infinity, and Overflow-Safe Checks
Key takeaways
std::numeric_limits is a template class that queries the limit values and properties of types provided by the C++ standard library. You can check the maximum/minimum value, precision, special values, etc. of each type at compile time.
What are numeric_limits?
std::numeric_limits is a template class that queries the limit values and properties of types provided by the C++ standard library. The maximum/minimum value, precision, special values, etc. of each type can be checked at compile time.
#include <limits>
#include <iostream>
using namespace std;
int main() {
cout << "int min: " << numeric_limits<int>::min() << endl;
cout << "int max: " << numeric_limits<int>::max() << endl;
cout << "double min: " << numeric_limits<double>::min() << endl;
cout << "double max: " << numeric_limits<double>::max() << endl;
}
Why do you need it?:
- Platform independence: Consistently query type limits that vary depending on the platform
- Type safety: Type-safe alternative to macros (
INT_MAX) - Compile time: No runtime overhead
- Standardization: Supports all standard types
// ❌ Macro: not type-safe, requires a header
#include <climits>
int max = INT_MAX;
// ✅ numeric_limits: type-safe, standardized
int max = std::numeric_limits<int>::max();
The macro form is a preprocessor token, not a typed value — INT_MAX has no type of its own until it’s substituted, so it can’t participate in template argument deduction or overload resolution the way numeric_limits<T>::max() can. Write a generic function that needs “the max value of whatever type T is,” and macros simply have no way to parameterize on T — numeric_limits<T>::max() is precisely what makes the safe-cast and sentinel-value patterns later in this guide possible.
Main member functions:
| member function | Description | Example |
|---|---|---|
min() | Minimum value (integer) or minimum positive number (real number) | numeric_limits<int>::min() |
max() | maximum value | numeric_limits<int>::max() |
lowest() | Minimum value (including real numbers) | numeric_limits<double>::lowest() |
infinity() | infinity (real numbers only) | numeric_limits<double>::infinity() |
quiet_NaN() | NaN (only real numbers) | numeric_limits<double>::quiet_NaN() |
Major member constants:
| member constant | Description | Example |
|---|---|---|
is_signed | Is it a signed type? | numeric_limits<int>::is_signed |
is_integer | Is it an integer type? | numeric_limits<int>::is_integer |
is_exact | Is this an accurate expression? | numeric_limits<int>::is_exact |
has_infinity | Whether to support infinity | numeric_limits<double>::has_infinity |
digits10 | decimal precision | numeric_limits<double>::digits10 |
Integer limits
// char
cout << "char min: " << (int)numeric_limits<char>::min() << endl;
cout << "char max: " << (int)numeric_limits<char>::max() << endl;
// short
cout << "short min: " << numeric_limits<short>::min() << endl;
cout << "short max: " << numeric_limits<short>::max() << endl;
// int
cout << "int min: " << numeric_limits<int>::min() << endl;
cout << "int max: " << numeric_limits<int>::max() << endl;
// long long
cout << "long long min: " << numeric_limits<long long>::min() << endl;
cout << "long long max: " << numeric_limits<long long>::max() << endl;
The (int) cast on numeric_limits<char>::min()/max() isn’t decorative — char is a character type as far as << is concerned, so printing it directly would print the character at that byte value (often an unprintable control character), not the number. Casting to int first forces operator<< to pick the integer overload, which is why this cast shows up constantly around char and numeric_limits together.
Floating-point limits and digits10
// float
cout << "float min: " << numeric_limits<float>::min() << endl;
cout << "float max: " << numeric_limits<float>::max() << endl;
cout << "float precision (digits10): " << numeric_limits<float>::digits10 << endl;
// double
cout << "double min: " << numeric_limits<double>::min() << endl;
cout << "double max: " << numeric_limits<double>::max() << endl;
cout << "double precision (digits10): " << numeric_limits<double>::digits10 << endl;
digits10 answers a genuinely useful question that’s easy to get wrong by intuition: how many decimal digits can this type represent and round-trip without losing precision. float’s digits10 is 6, double’s is 15 — which is the concrete, standard-guaranteed way to know, for example, that a float can’t reliably hold something like a 9-digit invoice number without silent precision loss, well before that shows up as a “why is my total off by a cent” bug report.
infinity, NaN, and the check functions
// Infinity
double inf = numeric_limits<double>::infinity();
cout << inf << endl; // inf
// NaN
double nan = numeric_limits<double>::quiet_NaN();
cout << nan << endl; // nan
// Checking
cout << isinf(inf) << endl; // 1
cout << isnan(nan) << endl; // 1
The check functions matter because of one of IEEE 754’s stranger properties: NaN is not equal to anything, including itself — nan == nan evaluates to false. That means if (x == numeric_limits<double>::quiet_NaN()) silently never triggers, no matter what x holds, which is exactly the kind of bug that passes a casual code review. isnan(x) is the only correct way to test for NaN; reaching for == here is a trap specific to floating-point that doesn’t exist for any other type in this guide.
Overflow-safe addition, range checks, and seeds for min/max
Overflow-safe addition
bool safeAdd(int a, int b, int& result) {
if (a > 0 && b > numeric_limits<int>::max() - a) {
return false; // would overflow
}
if (a < 0 && b < numeric_limits<int>::min() - a) {
return false; // would underflow
}
result = a + b;
return true;
}
int main() {
int result;
if (safeAdd(INT_MAX, 1, result)) {
cout << result << endl;
} else {
cout << "Overflow" << endl;
}
}
The check b > numeric_limits<int>::max() - a looks like it’s just rearranging a + b > max(), but that rearrangement is the entire point: computing a + b directly when it overflows is undefined behavior for signed integers, so the check has to be structured to detect the overflow before the addition happens, using subtraction (which can’t itself overflow given the preceding sign check) instead. This pattern — rewrite the risky operation as an equivalent check that avoids ever performing the risky operation — is the standard idiom for safe integer arithmetic in C++, since signed overflow can’t be caught after the fact the way it can with unsigned wraparound.
A generic range check
template<typename T>
bool inRange(double value) {
return value >= numeric_limits<T>::min() &&
value <= numeric_limits<T>::max();
}
int main() {
double x = 300.0;
cout << "fits in char: " << inRange<char>(x) << endl; // 0
cout << "fits in short: " << inRange<short>(x) << endl; // 1
cout << "fits in int: " << inRange<int>(x) << endl; // 1
}
This generic inRange<T> works for any integer type without being rewritten per type, which is the practical payoff of numeric_limits being a template rather than a set of type-specific macros — the same function body serves char, short, int, or any custom type that specializes numeric_limits (shown in the FAQ below), with the comparison bounds resolved at compile time for each instantiation.
Seeding a running minimum and maximum
template<typename T>
T findMin(const vector<T>& v) {
if (v.empty()) {
return numeric_limits<T>::max();
}
T minVal = numeric_limits<T>::max();
for (T x : v) {
if (x < minVal) {
minVal = x;
}
}
return minVal;
}
int main() {
vector<int> v = {5, 2, 8, 1, 9};
cout << "min: " << findMin(v) << endl; // 1
}
Seeding minVal with numeric_limits<T>::max() is a small but genuinely idiomatic pattern: any real value in v is guaranteed to be less than or equal to the type’s maximum, so the very first comparison in the loop is guaranteed to succeed and set minVal to a real element. The alternative — seeding with v[0] and starting the loop from index 1 — works too, but needs an extra branch to handle the empty-vector case separately; seeding with max() handles “no elements smaller than infinity” and “empty vector” with the same code path.
Printing a type’s properties
template<typename T>
void printTypeInfo() {
cout << "type: " << typeid(T).name() << endl;
cout << "min: " << numeric_limits<T>::min() << endl;
cout << "max: " << numeric_limits<T>::max() << endl;
cout << "signed: " << (numeric_limits<T>::is_signed ? "yes" : "no") << endl;
cout << "integer: " << (numeric_limits<T>::is_integer ? "yes" : "no") << endl;
if (!numeric_limits<T>::is_integer) {
cout << "precision (digits10): " << numeric_limits<T>::digits10 << endl;
cout << "has infinity: " << numeric_limits<T>::has_infinity << endl;
}
cout << endl;
}
int main() {
printTypeInfo<int>();
printTypeInfo<unsigned int>();
printTypeInfo<float>();
printTypeInfo<double>();
}
typeid(T).name() is worth a caveat here since it’s easy to assume it prints something clean like "int" — the standard only guarantees the string is distinct per type, not human-readable. GCC and Clang typically emit mangled names (i for int, f for float), while MSVC emits closer-to-readable names; if you need a portable, readable type name for debugging output, typeid(T).name() alone isn’t it — you’d want a demangling helper or a manual type-name trait.
is_exact, is_signed, and other type properties
// signed
numeric_limits<int>::is_signed // true
numeric_limits<unsigned>::is_signed // false
// integer
numeric_limits<int>::is_integer // true
numeric_limits<double>::is_integer // false
// exact
numeric_limits<int>::is_exact // true
numeric_limits<float>::is_exact // false
// infinity
numeric_limits<double>::has_infinity // true
numeric_limits<int>::has_infinity // false
is_exact is the property most people haven’t heard of but that explains a lot of floating-point “weirdness” at a glance: it’s true for integer types (every representable value is exact — no rounding) and false for float/double (most decimal values, like 0.1, have no exact binary floating-point representation). Any time 0.1 + 0.2 != 0.3 surprises someone, is_exact being false for double is the underlying, standard-documented reason why.
min() vs lowest(), unchecked overflow, and unsigned tautologies
min() is not the most negative float
// ❌ For floating-point types, min() is the smallest *positive* value
cout << numeric_limits<double>::min() << endl; // 2.22507e-308 (a tiny positive number)
// ✅ The true minimum (most negative) value is lowest()
cout << numeric_limits<double>::lowest() << endl; // -1.79769e+308
This asymmetry between integer and floating-point types is the single most common numeric_limits mistake, and it’s not arbitrary — min() for a floating-point type means “smallest positive normalized value,” a concept inherited from the IEEE 754 standard, which is a genuinely different thing from “most negative representable value.” lowest() was added specifically in C++11 to give floating-point types a way to express the latter without ambiguity; for integer types, the two happen to coincide, which is exactly what makes it easy to forget they diverge for float/double.
Signed overflow without a check
// ❌ Ignoring overflow
int x = INT_MAX;
x++; // undefined behavior
// ✅ Check first
if (x < numeric_limits<int>::max()) {
x++;
}
Signed integer overflow being undefined behavior (rather than well-defined wraparound, as unsigned has) means the compiler is legally permitted to assume it never happens — and optimizers take that assumption seriously. A sufficiently aggressive compiler can, and in real cases has, eliminated an overflow check entirely if it can prove the check is only reachable after overflow already occurred, because from the optimizer’s perspective that code path is unreachable. Checking before incrementing, as shown here, is the only way to reliably guard against it.
Comparisons that are always true for unsigned
// ❌ This check is always true and therefore useless
unsigned int x = 10;
if (x >= 0) {
// ...
}
// ✅ Ask numeric_limits instead of assuming
if (numeric_limits<unsigned int>::is_signed) {
// this branch never executes for unsigned int
}
x >= 0 for an unsigned int is a tautology the compiler will often flag with a warning (-Wtype-limits in GCC/Clang) precisely because it can never be false — there’s no negative unsigned value to compare against. This is a real bug pattern in code ported from a signed type to unsigned without re-checking the comparisons that assumed negative values were possible; numeric_limits<T>::is_signed is the generic, compile-time-checkable way to write code that needs to know which case it’s in without hardcoding an assumption about T.
Safe conversions, sentinels, and range validation
Checked narrowing conversions
template<typename Target, typename Source>
std::optional<Target> safeCast(Source value) {
if (value < static_cast<Source>(std::numeric_limits<Target>::min()) ||
value > static_cast<Source>(std::numeric_limits<Target>::max())) {
return std::nullopt;
}
return static_cast<Target>(value);
}
// Usage
auto result = safeCast<int>(300L);
if (result) {
std::cout << "Conversion succeeded: " << *result << '\n';
} else {
std::cout << "Conversion failed: value out of range\n";
}
Casting Target’s bounds into Source’s type before comparing (rather than casting value into Target first and comparing there) is deliberate — casting a too-large value into Target first would already have triggered the very overflow this function exists to prevent, before the check ever runs. Comparing in Source’s domain, where value is guaranteed to be representable without loss, is what makes this check actually safe rather than checking after the damage is done.
Sentinel values (and why optional is better)
template<typename T>
class OptionalValue {
T value_;
static constexpr T SENTINEL = std::numeric_limits<T>::max();
public:
OptionalValue() : value_(SENTINEL) {}
OptionalValue(T value) : value_(value) {}
bool hasValue() const {
return value_ != SENTINEL;
}
T value() const {
if (!hasValue()) {
throw std::runtime_error("No value");
}
return value_;
}
};
// Usage
OptionalValue<int> opt;
if (opt.hasValue()) {
std::cout << opt.value() << '\n';
}
This sentinel-value pattern predates std::optional and is worth recognizing precisely so you can replace it — using numeric_limits<T>::max() as a stand-in for “no value” silently breaks the moment a legitimate value equal to that sentinel needs to be stored, which is a real risk for types with a small range. std::optional<T> (used correctly in the checked-conversion pattern above) represents absence as a genuinely separate state rather than overloading a magic value, and should be the default choice for new code; this pattern mainly shows up maintaining older codebases or interfacing with APIs that predate std::optional.
Validating a value against a type’s range
template<typename T>
class BoundedValue {
T value_;
public:
BoundedValue(T value) {
if (value < std::numeric_limits<T>::min() ||
value > std::numeric_limits<T>::max()) {
throw std::out_of_range("Value out of range");
}
value_ = value;
}
T get() const { return value_; }
};
// Usage
try {
BoundedValue<int> val(42);
std::cout << val.get() << '\n';
} catch (const std::out_of_range& e) {
std::cerr << e.what() << '\n';
}
This particular check is actually a no-op for any T where the constructor parameter’s type already is T — a T value argument can never hold a value outside T’s own range, so value < numeric_limits<T>::min() can never be true. This pattern earns its keep when the constructor instead takes a wider type (say, BoundedValue(long value) constructing a BoundedValue<int>), where the incoming value genuinely can exceed the target type’s range — as written here with a same-type parameter, it’s mostly documentation of intent rather than an active safety check.
FAQ
Q1: When do you use numeric_limits?
A:
- Overflow/underflow check
- Initial value setting (reset to maximum/minimum values)
- Check type information (sign, integer, etc.)
- Range verification (before type conversion)
// Overflow check
if (x > std::numeric_limits<int>::max() - y) {
// overflow would occur
}
// Initial value setting
int minVal = std::numeric_limits<int>::max();
Q2: min() vs lowest()?
A:
- integer:
min()andlowest()are the same - Real:
min()is the minimum positive number (value close to 0),lowest()is the minimum value (negative number)
// integer
std::cout << std::numeric_limits<int>::min() << '\n'; // -2147483648
std::cout << std::numeric_limits<int>::lowest() << '\n'; // -2147483648
// floating-point
std::cout << std::numeric_limits<double>::min() << '\n'; // 2.22507e-308 (smallest positive)
std::cout << std::numeric_limits<double>::lowest() << '\n'; // -1.79769e+308 (true minimum)
Q3: What is the performance overhead?
A: None. All members of numeric_limits are compile-time constants, so there is no runtime overhead whatsoever.
// The value is determined at compile time
constexpr int maxInt = std::numeric_limits<int>::max();
Q4: What is custom type?
A: You can support custom types by specializing numeric_limits.
struct MyInt {
int value;
};
namespace std {
template<>
struct numeric_limits<MyInt> {
static constexpr bool is_specialized = true;
static constexpr MyInt min() { return MyInt{-100}; }
static constexpr MyInt max() { return MyInt{100}; }
};
}
// Usage
MyInt maxVal = std::numeric_limits<MyInt>::max();
Q5: Is it platform independent?
A: Yes, numeric_limits will be automatically set for your platform. Returns different values on different systems.
// int is 32 bits on essentially every mainstream platform today
std::cout << std::numeric_limits<int>::max() << '\n'; // 2147483647
// long, however, varies: 32 bits on Windows (LLP64), 64 bits on Linux/macOS (LP64)
std::cout << std::numeric_limits<long>::max() << '\n';
// Windows: 2147483647
// Linux/macOS: 9223372036854775807
Q6: When do you use infinity() and NaN?
A: Used to express special values in floating-point computations.
double inf = std::numeric_limits<double>::infinity();
double nan = std::numeric_limits<double>::quiet_NaN();
// Checking
if (std::isinf(inf)) {
std::cout << "Infinite\n";
}
if (std::isnan(nan)) {
std::cout << "NaN\n";
}
// How they normally arise
double result = 1.0 / 0.0; // inf
double invalid = 0.0 / 0.0; // nan
Q7: What are the numeric_limits learning resources?
A:
- cppreference.com - std::numeric_limits
- “The C++ Standard Library” (2nd Edition) by Nicolai Josuttis
- “Effective C++” (3rd Edition) by Scott Meyers
numeric_limits is a standard library template that looks up the limits and properties of a type at compile time.
Related Articles
- C++ Iterators
- C++ Templates | Beginner’s Guide to Generic Programming
- How <type_traits> Works
- The Adapter Pattern in C++
- C++ ADL (Argument-Dependent Lookup)