C++ / STANDARD CONTAINERS
Numeric work with cmath and numeric_limits
Use cmath's rounding and math functions correctly and read numeric_limits to build magnitude-aware float comparisons and overflow checks.
What you will learn
- Build float comparisons from numeric_limits epsilon scaled to operand magnitude
- Seed a maximum scan with lowest(), not min(), which is a tiny positive value
- Choose between floor, ceil, trunc, and round by the direction each one moves
- Detect inf and NaN with std::isfinite and std::isnan, never with ==
Understanding Numeric work with cmath and numeric_limits
<cmath> brings the C math library into C++ and adds float and long double overloads, so std::sqrt(2.0) computes in double while std::sqrt(2.0f) stays in float, and an integer argument promotes to double. The overload set matters: std::fabs and the <cmath> std::abs handle doubles, but the abs inherited from <cstdlib> takes an int, so if that is the declaration in scope, abs(-2.7) silently becomes 2. Accuracy is also not uniform across the header: IEEE-754 requires sqrt and the four arithmetic operators to return the correctly rounded result, while pow, exp and the trigonometric functions may be off in the last bit, which is why the same expression can print differently on two platforms.
std::numeric_limits<T> is the compile-time answer sheet for a type: how large, how small, how many digits, and whether IEEE rules apply. epsilon() is the member you reach for most often, 2.22e-16 for double, which is 2^-52, the distance from 1.0 to the next representable double. Read it as a relative gap rather than an absolute one: near 1000.0 neighbouring doubles are about a thousand times further apart, and near 1e9 the gap is roughly 1e-7. Two traps hide here: min() for a floating-point type is the smallest positive normal value, not the most negative one (that is lowest()), and for integer types min() and lowest() are identical, so code that confuses them passes its int tests.
Put the two headers together and comparison stops being guesswork: the gap you accept is a small multiple of epsilon times the magnitude of the values compared, which is why sqrt(2)*sqrt(2) fails == 2.0 yet passes a scaled test. The special values follow the same logic. A double that overflows becomes infinity instead of wrapping, 0.0/0.0 and inf-inf produce NaN, and NaN compares unequal to everything including itself, so an == test for NaN can never fire and std::isnan or std::isfinite is the only reliable check. That reaches into container code: a NaN key violates the strict weak ordering std::map, std::set and std::sort assume, so screening values with std::isfinite as they enter a container is cheaper than debugging a corrupted ordering later.
placeholder
<cmath>
<iomanip>
<iostream>
<limits>
// Tolerance measured in units of epsilon, scaled to the size of the operands.
bool nearly_equal(double a, double b, double ulps = 4.0) {
const double gap = std::fabs(a - b);
const double scale = std::fmax(std::fabs(a), std::fabs(b));
return gap <= ulps * std::numeric_limits<double>::epsilon() * scale;
}
int main() {
const double sum = 0.1 + 0.2;
const double root = std::sqrt(2.0) * std::sqrt(2.0);
std::cout << std::setprecision(17);
std::cout << "0.1 + 0.2 = " << sum << '\n';
std::cout << "0.3 = " << 0.3 << '\n';
std::cout << "sqrt(2)*sqrt(2) = " << root << '\n';
std::cout << std::setprecision(6) << std::boolalpha;
std::cout << "sum == 0.3 : " << (sum == 0.3) << '\n';
std::cout << "nearly_equal : " << nearly_equal(sum, 0.3) << '\n';
std::cout << "root == 2.0 : " << (root == 2.0) << '\n';
std::cout << "nearly_equal : " << nearly_equal(root, 2.0) << '\n';
std::cout << "double::epsilon = " << std::numeric_limits<double>::epsilon() << '\n';
std::cout << "double::lowest = " << std::numeric_limits<double>::lowest() << '\n';
std::cout << "double::min = " << std::numeric_limits<double>::min() << '\n';
const double inf = std::numeric_limits<double>::infinity();
std::cout << "isfinite(inf) : " << std::isfinite(inf) << '\n';
std::cout << "isnan(inf - inf) : " << std::isnan(inf - inf) << '\n';
}
Floating-point error is relative, so tolerances, bounds and special-value checks should come from std::numeric_limits instead of hand-picked constants.
Worked examples
Four ways to remove a fraction
Shows how floor, ceil, trunc and round differ on positive and negative inputs, and how fmod differs from remainder.
<cmath>
<iomanip>
<iostream>
int main() {
const double xs[] = {2.5, -2.5, 2.4, -2.4};
std::cout << " x floor ceil trunc round\n";
std::cout << std::fixed << std::setprecision(1);
for (double x : xs) {
std::cout << std::setw(5) << x
<< std::setw(7) << std::floor(x)
<< std::setw(7) << std::ceil(x)
<< std::setw(7) << std::trunc(x)
<< std::setw(7) << std::round(x) << '\n';
}
std::cout << "fmod(5, 3) = " << std::fmod(5.0, 3.0) << '\n';
std::cout << "remainder(5, 3) = " << std::remainder(5.0, 3.0) << '\n';
}
Example explained
Line 1std::floor always moves toward negative infinity, so floor(-2.5) is -3.0, while std::trunc drops the fractional part and gives -2.0.
Line 2std::round breaks ties away from zero, which is why 2.5 becomes 3.0 and -2.5 becomes -3.0; std::nearbyint would give 2.0 and -2.0 under the default ties-to-even rounding mode.
Line 3All four return a double, so you still need a cast to get an integer, and that cast truncates rather than rounding.
Line 4std::fmod keeps the sign of the dividend (5 - 1*3 = 2) while std::remainder rounds the quotient to nearest (5 - 2*3 = -1).
min() destroys a maximum scan
Demonstrates why a maximum-finding loop must be seeded with lowest() and not min() when the data can be negative.
<iostream>
<limits>
<vector>
int main() {
const std::vector<double> temps = {-12.5, -3.25, -40.0, -7.5};
double from_min = std::numeric_limits<double>::min(); // smallest positive normal
double from_lowest = std::numeric_limits<double>::lowest(); // most negative finite
for (double t : temps) {
if (t > from_min) from_min = t;
if (t > from_lowest) from_lowest = t;
}
std::cout << "min() = " << std::numeric_limits<double>::min() << '\n';
std::cout << "lowest() = " << std::numeric_limits<double>::lowest() << '\n';
std::cout << "seeded with min() = " << from_min << '\n';
std::cout << "seeded with lowest() = " << from_lowest << '\n';
}
Example explained
Line 1numeric_limits<double>::min() is +2.22507e-308, the smallest positive normal value, so it sits above every negative sample.
Line 2Every t > from_min test therefore fails and the seed survives as the reported maximum, a plausible-looking tiny number instead of -3.25.
Line 3lowest() is the most negative finite double, which is the correct identity element for a maximum scan.
Line 4For int the two members return the same value, which is why this bug usually survives integer tests and only appears with doubles.
Integer limits and checking before you overflow
Uses numeric_limits<int> to test an addition for overflow before performing it, and contrasts signed with unsigned behaviour.
<iostream>
<limits>
// Signed overflow is undefined behaviour, so test before adding, not after.
bool adding_overflows(int a, int b) {
if (b > 0) return a > std::numeric_limits<int>::max() - b;
if (b < 0) return a < std::numeric_limits<int>::min() - b;
return false;
}
int main() {
const int hi = std::numeric_limits<int>::max();
const int lo = std::numeric_limits<int>::min();
std::cout << std::boolalpha;
std::cout << "int range : " << lo << " .. " << hi << '\n';
std::cout << "int digits : " << std::numeric_limits<int>::digits << '\n';
std::cout << "hi + 1 overflows : " << adding_overflows(hi, 1) << '\n';
std::cout << "hi - 1 overflows : " << adding_overflows(hi, -1) << '\n';
std::cout << "lo - 1 overflows : " << adding_overflows(lo, -1) << '\n';
std::cout << "unsigned max + 1 : " << std::numeric_limits<unsigned>::max() + 1u << '\n';
}
Example explained
Line 1The check subtracts b from the limit instead of adding b to a, so the test itself never overflows; computing hi + 1 to find out would already be undefined behaviour.
Line 2digits is 31 because it counts value bits only, not the sign bit, which is why the range is asymmetric: -2147483648 to 2147483647.
Line 3Unsigned arithmetic is defined to wrap modulo 2^32, so max() + 1u is a well-defined 0 while the signed equivalent is not.
Line 4These numbers describe a platform with 32-bit int; reading them from numeric_limits is what keeps the function correct where int is a different width.
Important notes
The 17 in setprecision(17) is not arbitrary: it is std::numeric_limits<double>::max_digits10, the digit count that lets any double round-trip through text. digits10 (15 for double) is the opposite direction, the decimal digits guaranteed to survive a trip through a double.
std::pow is not required to be correctly rounded, so static_cast<int>(std::pow(10, 2)) can be 99 on some implementations; write x * x for small powers and std::llround when you need an integer result.
Common mistakes
Seeding a maximum scan with std::numeric_limits<double>::min(): it is +2.2e-308, so on all-negative data the loop returns that tiny positive number as the maximum instead of the real largest value.
Using one fixed tolerance such as fabs(a - b) < 1e-9 at every scale: near 1e9 adjacent doubles are already about 1e-7 apart so the test can never pass, and near 1e-12 it declares completely unrelated values equal.
Testing for NaN with x == std::numeric_limits<double>::quiet_NaN(): NaN compares unequal to everything including itself, so the guard is always false, the NaN slips through, and every later sum or average built from it becomes NaN.
Try it yourself
Change, predict, then run
Using std::nextafter, print for x = 1.0, 1e6 and 1e9 the distance from x to the very next representable double, then decide for each whether a fixed tolerance of 1e-9 would call those two adjacent doubles equal. Confirm that a tolerance of 4 * numeric_limits<double>::epsilon() * x gives the same verdict at all three magnitudes.
Open the C++ workspaceCheck your understanding
A codebase compares doubles everywhere with std::fabs(a - b) < 1e-9. Where does this test break down, and why?
- Nowhere: 1e-9 is far larger than epsilon, so it is a safe bound at any magnitude.
- It breaks for large and tiny values: around 1e9 neighbouring doubles are already about 1e-7 apart so two distinct results can never fall within 1e-9, while around 1e-300 the test accepts values differing by many orders of magnitude.
- It breaks only for negative inputs, because fabs discards the sign bit and the subtraction underflows.
- It breaks because 1e-9 has no exact double representation, which makes the result of the comparison unspecified.
Show answer
Double precision is relative: with a 53-bit significand the absolute distance between neighbouring values scales with magnitude, roughly 1.2e-7 near 1e9 and roughly 1.4e-316 near 1e-300. A fixed 1e-9 is therefore unreachably strict at the top of the range and meaninglessly loose at the bottom, which is why the tolerance has to be epsilon times the operands' magnitude. Option 4 is tempting because 1e-9 really is stored as an approximation, but that approximation is within one ULP of 1e-9 and the comparison is perfectly well defined; the inexactness of the constant is not what breaks the test.