C++ Module 2 – Types, values and expressions
Floating-point precision and comparison in C++
Why 0.1 + 0.2 is not 0.3 in C++, how a double is stored, how to compare with a tolerance, print digits that round-trip, and keep money exact.
What you will learn
- Explain why 0.1 + 0.2 != 0.3 in binary floating point
- Compare floating-point values with a tolerance
- Format values with enough digits to round-trip
Before you start
What you’ll learn · Before you start
What you will learn
- Explain why 0.1 + 0.2 != 0.3 in binary floating point
- Compare floating-point values with a tolerance
- Format values with enough digits to round-trip
Before you start
On this page
A double stores a number in binary: on every mainstream platform it is the IEEE 754 binary64 format, with 53
significant bits and an exponent. Most decimal fractions, including 0.1, 0.2 and 0.3, have no exact binary form, so
each one is stored as the nearest double. The sum of the stored 0.1 and the stored 0.2 then lands one step above
the double nearest 0.3, and 0.1 + 0.2 == 0.3 is false. Nothing is broken, and C++ is not alone: every language
that uses this hardware format gets the same answer. The cure is to use floating point knowingly: compare with a
tolerance instead of ==, print enough digits to see what is really stored, test for NaN with std::isnan, and
count money in whole cents or paise. This lesson shows each of these on Apple clang 21 and GCC 16, and measures how
rounding errors grow when a loop adds up ten million values.
What a double stores
A double is 64 bits split into three fields: 1 sign bit, 11 bits of exponent and 52 bits of fraction. For a
normal number the fraction follows an implied leading 1, which is how 52 stored bits give 53 significant bits.
How a double stores 0.1
Text description of the diagram
A bar of 64 bits split into three fields, from left to right.
- Sign, 1 bit: 0, so the number is positive.
- Exponent, 11 bits: 01111111011, which is 1019. The stored exponent has a bias of 1023, so the number is scaled by 2 to the power 1019 − 1023 = −4.
- Fraction, 52 bits: 1001 1001 … 1010, which is 0x999999999999A in hexadecimal. A leading 1 is implied and not stored.
The value is (1 + 0x999999999999A / 2⁵²) × 2⁻⁴, which is 0.1000000000000000055511…, slightly more than 0.1.
std::bit_cast from <bit> (C++20) copies the bits of a double into an integer of the same size, so a program
can take the fields apart:
#include <bit>
#include <cstdint>
#include <limits>
#include <print>
// Splits a double into the three fields of the IEEE 754 binary64 format.
void fields(double x) {
auto bits = std::bit_cast<std::uint64_t>(x);
auto sign = bits >> 63;
auto exponent = (bits >> 52) & 0x7FF; // 11 bits, stored with a bias of 1023
auto fraction = bits & ((std::uint64_t{1} << 52) - 1); // 52 bits after the leading 1
std::println("{:<5} {:#018x} sign {} exponent {:>4} = 2^{:<3} fraction {:#015x}",
x, bits, sign, exponent, static_cast<int>(exponent) - 1023, fraction);
}
int main() {
static_assert(std::numeric_limits<double>::is_iec559); // IEEE 754 on this platform
fields(1.0);
fields(0.5);
fields(0.1);
fields(-2.0);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra bits.cpp -o bits
./bits
g++-16 -std=c++23 -Wall -Wextra bits.cpp -o bits
./bits
""") Output
$ clang++ -std=c++23 -Wall -Wextra bits.cpp -o bits $ ./bits 1 0x3ff0000000000000 sign 0 exponent 1023 = 2^0 fraction 0x0000000000000 0.5 0x3fe0000000000000 sign 0 exponent 1022 = 2^-1 fraction 0x0000000000000 0.1 0x3fb999999999999a sign 0 exponent 1019 = 2^-4 fraction 0x999999999999a -2 0xc000000000000000 sign 1 exponent 1024 = 2^1 fraction 0x0000000000000 $ g++-16 -std=c++23 -Wall -Wextra bits.cpp -o bits $ ./bits 1 0x3ff0000000000000 sign 0 exponent 1023 = 2^0 fraction 0x0000000000000 0.5 0x3fe0000000000000 sign 0 exponent 1022 = 2^-1 fraction 0x0000000000000 0.1 0x3fb999999999999a sign 0 exponent 1019 = 2^-4 fraction 0x999999999999a -2 0xc000000000000000 sign 1 exponent 1024 = 2^1 fraction 0x0000000000000
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bits_session.py
Each $ line is a command, exactly as typed at a terminal prompt, and the lines under it are its output. 1.0, 0.5
and -2.0 are exact: each is a power of two, so the fraction is all zeros, and only the exponent and the sign
change. 0.1 is 1/10, and a fraction is exact in binary only when its denominator is a power of two. 10 has a factor
of 5, so 0.1 in binary is 0.000110011001100…, repeating for ever; the double keeps the first 53 significant bits
and rounds the rest, which makes the repeating hexadecimal digits 999… end in a, rounded up. The stored value is
a little more than 0.1. The static_assert at the start of main checks
std::numeric_limits<double>::is_iec559, which is true when double is IEEE 754, so the program refuses to build
anywhere this description would be wrong.
Why 0.1 + 0.2 is not 0.3
#include <cmath>
#include <iostream>
#include <print>
int main() {
double sum = 0.1 + 0.2;
std::println("sum == 0.3: {}", sum == 0.3);
std::println("{{}}: {} {} {}", 0.1, 0.3, sum);
std::println("{{:.17g}}: {:.17g} {:.17g} {:.17g}", 0.1, 0.3, sum);
std::cout << "std::cout: " << 0.1 << ' ' << 0.3 << ' ' << sum << '\n';
std::println("next double above 0.3: {}", std::nextafter(0.3, 1.0));
std::println("shortest form of 1000000.0 and 1234567.0: {} {}", 1000000.0, 1234567.0);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra point_one.cpp -o point_one
./point_one
g++-16 -std=c++23 -Wall -Wextra point_one.cpp -o point_one
./point_one
""") Output
$ clang++ -std=c++23 -Wall -Wextra point_one.cpp -o point_one
$ ./point_one
sum == 0.3: false
{}: 0.1 0.3 0.30000000000000004
{:.17g}: 0.10000000000000001 0.29999999999999999 0.30000000000000004
std::cout: 0.1 0.3 0.3
next double above 0.3: 0.30000000000000004
shortest form of 1000000.0 and 1234567.0: 1e+06 1234567
$ g++-16 -std=c++23 -Wall -Wextra point_one.cpp -o point_one
$ ./point_one
sum == 0.3: false
{}: 0.1 0.3 0.30000000000000004
{:.17g}: 0.10000000000000001 0.29999999999999999 0.30000000000000004
std::cout: 0.1 0.3 0.3
next double above 0.3: 0.30000000000000004
shortest form of 1000000.0 and 1234567.0: 1e+06 1234567
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 point_one_session.py
Three roundings are involved: 0.1 and 0.2 are each rounded when the program is compiled, and their sum is rounded
again when it is computed. The result is 0.30000000000000004, which is exactly the next double above the one that
the literal 0.3 is stored as, the value std::nextafter printed. One step apart is enough for == to say false.
The three printing styles of the session are worth knowing:
std::println("{}", x)prints the shortest text that reads back as exactly the samedouble. The standard defines it throughstd::to_chars: the smallest number of characters that recovers the value. That is why it shows 0.1 for 0.1 and 0.30000000000000004 for the sum: each is the shortest text for its own value. Shortest can mean scientific notation, so 1000000.0 prints as1e+06; ask for a format such as{:.2f}when a reader needs ordinary digits.{:.17g}prints 17 significant digits, whichstd::numeric_limits<double>::max_digits10says is always enough to tell twodoublevalues apart. It shows that 0.1 is stored as 0.10000000000000001 and 0.3 as 0.29999999999999999.std::coutprints 6 significant digits unless you set a precision, so the sum comes out as 0.3: the difference is there, but the stream rounds it away. Many confusing debugging sessions start this way.
Comparing floating-point values
Because computed values carry small rounding errors, compare them with a tolerance. A relative tolerance accepts a difference that is small compared with the numbers themselves, so it works the same for 0.3 and for 10²⁰. An absolute tolerance accepts a difference below a fixed size, which is the only test that can accept a value near zero, since nothing but zero is relatively close to zero.
#include <algorithm>
#include <cmath>
#include <limits>
#include <print>
// True when a and b differ by at most rel times the larger of the two, or by at most abs.
// The relative test scales with the numbers; the absolute one handles values near zero.
bool nearly_equal(double a, double b, double rel = 1e-9, double abs = 1e-12) {
if (a == b) return true; // also equal infinities
if (!std::isfinite(a) || !std::isfinite(b)) return false;
double diff = std::fabs(a - b);
return diff <= abs || diff <= rel * std::max(std::fabs(a), std::fabs(b));
}
int main() {
double sum = 0.1 + 0.2;
double balance = 0.1 + 0.2 - 0.3; // should be 0, is a tiny residue
double inf = std::numeric_limits<double>::infinity();
double nan = std::numeric_limits<double>::quiet_NaN();
std::println("balance = {}", balance);
std::println("nearly_equal(0.1 + 0.2, 0.3) {}", nearly_equal(sum, 0.3));
std::println("nearly_equal(balance, 0.0) {}", nearly_equal(balance, 0.0));
std::println("nearly_equal(balance, 0.0, 1e-9, 0.0) {}", nearly_equal(balance, 0.0, 1e-9, 0.0));
std::println("nearly_equal(1e20, 1e20 + 1e5) {}", nearly_equal(1e20, 1e20 + 1e5));
std::println("nearly_equal(1.0, 1.001) {}", nearly_equal(1.0, 1.001));
std::println("nearly_equal(inf, inf) {}", nearly_equal(inf, inf));
std::println("nearly_equal(nan, nan) {}", nearly_equal(nan, nan));
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra compare.cpp -o compare
./compare
g++-16 -std=c++23 -Wall -Wextra compare.cpp -o compare
./compare
""") Output
$ clang++ -std=c++23 -Wall -Wextra compare.cpp -o compare $ ./compare balance = 5.551115123125783e-17 nearly_equal(0.1 + 0.2, 0.3) true nearly_equal(balance, 0.0) true nearly_equal(balance, 0.0, 1e-9, 0.0) false nearly_equal(1e20, 1e20 + 1e5) true nearly_equal(1.0, 1.001) false nearly_equal(inf, inf) true nearly_equal(nan, nan) false $ g++-16 -std=c++23 -Wall -Wextra compare.cpp -o compare $ ./compare balance = 5.551115123125783e-17 nearly_equal(0.1 + 0.2, 0.3) true nearly_equal(balance, 0.0) true nearly_equal(balance, 0.0, 1e-9, 0.0) false nearly_equal(1e20, 1e20 + 1e5) true nearly_equal(1.0, 1.001) false nearly_equal(inf, inf) true nearly_equal(nan, nan) false
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare_session.py
The function accepts the sum as 0.3. The residue balance, which should be 0 and is about 5.6 × 10⁻¹⁷, passes only
because of the absolute tolerance: with abs set to 0, the relative test alone rejects it. 10²⁰ and 10²⁰ + 10⁵ are
equal to about 15 digits, so they pass, while 1.0 and 1.001 differ in the third decimal and do not. Two infinities of
the same sign compare equal, and NaN is close to nothing, not even itself.
Choose the tolerances from the problem, not from the type. A relative tolerance of 10⁻⁹ asks for about nine matching
digits, plenty after a few operations on double values. The absolute tolerance should be far below anything that
matters in your units: 10⁻¹² of a rupee or a metre is far below anything that can be counted or measured. Avoid
std::numeric_limits<double>::epsilon() as a universal tolerance: it is the gap between 1.0 and the next double,
about 2.2 × 10⁻¹⁶, and means nothing for numbers far from 1.
Errors add up
Every operation rounds its result to the nearest representable value, and in a long loop the errors accumulate. This
program adds 0.1 ten million times, in a float, in a double, and with Kahan summation, which carries the part
of each addition that rounding dropped into the next one:
#include <print>
int main() {
constexpr int n = 10'000'000;
float in_float = 0.0f;
double in_double = 0.0;
double kahan = 0.0;
double lost = 0.0; // Kahan summation: the low-order part each addition dropped
for (int i = 0; i < n; ++i) {
in_float += 0.1f;
in_double += 0.1;
double y = 0.1 - lost;
double t = kahan + y;
lost = (t - kahan) - y;
kahan = t;
}
std::println("0.1 added {} times:", n);
std::println("float: {:.6f}", in_float);
std::println("double: {:.10f}", in_double);
std::println("Kahan: {:.10f}", kahan);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra accumulate.cpp -o accumulate
./accumulate
g++-16 -std=c++23 -Wall -Wextra accumulate.cpp -o accumulate
./accumulate
""") Output
$ clang++ -std=c++23 -Wall -Wextra accumulate.cpp -o accumulate $ ./accumulate 0.1 added 10000000 times: float: 1087937.000000 double: 999999.9998389754 Kahan: 1000000.0000000000 $ g++-16 -std=c++23 -Wall -Wextra accumulate.cpp -o accumulate $ ./accumulate 0.1 added 10000000 times: float: 1087937.000000 double: 999999.9998389754 Kahan: 1000000.0000000000
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 accumulate_session.py
The exact answer is 1,000,000. The float total is 1,087,937, almost 9 % too high. A float has 24 significant
bits, so the gap between neighbouring float values grows with the total: above 524,288 (2¹⁹) it is 0.0625, and each
0.1 added is rounded to the nearest multiple of that gap, which is 0.125. From that point every addition counts 25 %
too much. The double total misses by about 0.00016, because its 53 bits make the gaps about 500 million times smaller.
Kahan summation recovers the exact result at the cost of four operations per value instead of one.
Two cautions go with it. -ffast-math lets the compiler reorder floating-point operations as if they were exact,
which GCC’s manual warns can change results, and reordering is exactly what turns Kahan’s correction into zero, so
code that relies on it must not be built that way. And for one-off sums the simplest fix is often a wider type: add
float data in a double.
Infinity, NaN and signed zero
IEEE 754 has values that are not ordinary numbers, and C++ exposes them through std::numeric_limits:
#include <cmath>
#include <limits>
#include <print>
int main() {
const double inf = std::numeric_limits<double>::infinity();
const double nan = std::numeric_limits<double>::quiet_NaN();
std::println("x | x == x | x != x | x < 1 | x > 1 | std::isnan(x)");
for (double x : {1.0, inf, nan}) {
std::println("{:<4} | {:<6} | {:<6} | {:<5} | {:<5} | {}", x, x == x, x != x, x < 1, x > 1, std::isnan(x));
}
std::println("");
std::println("inf > max(): {}, 1 / inf = {}", inf > std::numeric_limits<double>::max(), 1 / inf);
std::println("-0.0 == 0.0: {}, but std::signbit(-0.0) is {}", -0.0 == 0.0, std::signbit(-0.0));
std::println("smallest positive double: {}", std::numeric_limits<double>::denorm_min());
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra special.cpp -o special
./special
g++-16 -std=c++23 -Wall -Wextra special.cpp -o special
./special
""") Output
$ clang++ -std=c++23 -Wall -Wextra special.cpp -o special $ ./special x | x == x | x != x | x < 1 | x > 1 | std::isnan(x) 1 | true | false | false | false | false inf | true | false | false | true | false nan | false | true | false | false | true inf > max(): true, 1 / inf = 0 -0.0 == 0.0: true, but std::signbit(-0.0) is true smallest positive double: 5e-324 $ g++-16 -std=c++23 -Wall -Wextra special.cpp -o special $ ./special x | x == x | x != x | x < 1 | x > 1 | std::isnan(x) 1 | true | false | false | false | false inf | true | false | false | true | false nan | false | true | false | false | true inf > max(): true, 1 / inf = 0 -0.0 == 0.0: true, but std::signbit(-0.0) is true smallest positive double: 5e-324
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 special_session.py
- Infinity is larger than every finite value, and
1 / infis 0. - NaN, “not a number”, is the result of operations that have no sensible answer, such as infinity minus
infinity. Every ordered comparison with a NaN is false and
==is false even with itself, so the table’s NaN row is false everywhere except inx != x. Test for it withstd::isnan(x), and for “an ordinary number” withstd::isfinite(x). A NaN in a sort key or astd::setbreaks the ordering those containers depend on. - -0.0 compares equal to 0.0, but
std::signbitcan tell them apart. - Below the smallest normal value lie the subnormal numbers, which give up precision to reach as low as about 4.9 × 10⁻³²⁴.
Create infinities with std::numeric_limits<double>::infinity(), not with 1.0 / 0.0: the C++ standard says
division by zero is undefined for every arithmetic type, even though IEEE 754 hardware returns an infinity, and
Clang’s UBSan can report it with -fsanitize=float-divide-by-zero.
Money: count in the smallest unit
Accounts and prices are decimal, and a decimal amount such as 0.10 is not exact in binary, so money stored as a
double drifts. Store whole units of the smallest coin instead, cents or paise, in a 64-bit integer:
#include <cstdint>
#include <print>
int main() {
// One hundred million payments of 0.10, added as a double and as whole paise.
double total = 0.0;
std::int64_t total_paise = 0;
for (int i = 0; i < 100'000'000; ++i) {
total += 0.10;
total_paise += 10;
}
std::println("double: {:.2f} (stored as {})", total, total);
std::println("paise: {}.{:02}", total_paise / 100, total_paise % 100);
// 7.5 % interest a year for 10 years on 100000.00, rounded to the nearest paisa every year.
std::int64_t balance = 100'000'00; // in paise
for (int year = 1; year <= 10; ++year) {
balance += (balance * 75 + 500) / 1000; // 7.5 % = 75 / 1000, with +500 to round half up
}
std::println("after 10 years: {}.{:02}", balance / 100, balance % 100);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra money.cpp -o money
./money
g++-16 -std=c++23 -Wall -Wextra money.cpp -o money
./money
""") Output
$ clang++ -std=c++23 -Wall -Wextra money.cpp -o money $ ./money double: 9999999.98 (stored as 9999999.98112945) paise: 10000000.00 after 10 years: 206103.16 $ g++-16 -std=c++23 -Wall -Wextra money.cpp -o money $ ./money double: 9999999.98 (stored as 9999999.98112945) paise: 10000000.00 after 10 years: 206103.16
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 money_session.py
After one hundred million payments of 0.10, the double total is 9,999,999.98112945, and rounded to two decimals
the total is two paise short of the 10,000,000.00 that the integer count gives exactly. A std::int64_t holds about
9.2 × 10¹⁸ paise, roughly 92 quadrillion rupees, so range is no concern. Integer money also makes rounding a decision
you write down: the interest calculation rounds to the nearest paisa every year, half up, by adding 500 before the
division by 1,000. Ten years at 7.5 % turn 100,000.00 into 206,103.16, and anyone can reproduce that figure paisa for
paisa, which is the property an account needs.
Narrower and wider types
float has 24 significant bits, about 7 decimal digits, and saves memory in large arrays. long double depends on
the platform: on Apple silicon it is identical to double, which Apple documents as a difference from the standard
Arm ABI. C++23 adds the optional types of <stdfloat> from proposal P1467R9, such as std::float16_t, a 16-bit
type used in graphics and machine learning:
#include <limits>
#include <print>
#if __has_include(<stdfloat>)
#include <stdfloat>
#endif
int main() {
std::println("long double: {} bytes, {} significant bits", sizeof(long double), std::numeric_limits<long double>::digits);
#if defined(__STDCPP_FLOAT16_T__)
std::float16_t h = 0.1f16;
std::println("std::float16_t: {} bytes; 0.1 is stored as {}", sizeof(h), static_cast<double>(h));
#else
std::println("no std::float16_t in this standard library");
#endif
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra half_precision.cpp -o half_precision
./half_precision
g++-16 -std=c++23 -Wall -Wextra half_precision.cpp -o half_precision
./half_precision
""") Output
$ clang++ -std=c++23 -Wall -Wextra half_precision.cpp -o half_precision $ ./half_precision long double: 8 bytes, 53 significant bits no std::float16_t in this standard library $ g++-16 -std=c++23 -Wall -Wextra half_precision.cpp -o half_precision $ ./half_precision long double: 8 bytes, 53 significant bits std::float16_t: 2 bytes; 0.1 is stored as 0.0999755859375
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 half_precision_session.py
Both compilers report an 8-byte long double with 53 significant bits, the same as double. GCC 16 provides
std::float16_t, and its 11 significant bits store 0.1 as 0.0999755859375, correct to only about three digits.
Apple clang 21’s libc++ has no <stdfloat> header, and libc++’s status page shows P1467R9 as not implemented, so
the file checks with __has_include and the macro __STDCPP_FLOAT16_T__ before using it.
Interview questions
Fresher · Why is 0.1 + 0.2 == 0.3 false in C++?
double is a binary format, and 0.1, 0.2 and 0.3 have no exact binary representation, so each literal is stored as
the nearest double: 0.1 and 0.2 slightly above their true values, 0.3 slightly below. Their sum is rounded once
more, and the result is the double just above the one stored for 0.3, so == compares two different values and
says false. std::println("{}", 0.1 + 0.2) shows 0.30000000000000004. The fix is a comparison with a tolerance,
relative for ordinary values plus an absolute one near zero, and integer cents or paise when the values are money.
It is not a C++ quirk: any language that uses IEEE 754 doubles gives the same result.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Key takeaways
doubleis IEEE 754 binary64 here: 1 sign bit, 11 exponent bits, 52 fraction bits, 53 significant bits.- Decimal fractions such as 0.1 are rounded to the nearest
double, so0.1 + 0.2is thedoublejust above 0.3. {}prints the shortest text that reads back exactly,{:.17g}always distinguishes two doubles, andstd::coutrounds to 6 digits by default.- Compare with a relative tolerance plus an absolute one near zero, never with
==and never withepsilon()alone. - Rounding errors accumulate: ten million additions of 0.1 gave 1,087,937 in a
float; Kahan summation or a wider type fixes it, and-ffast-mathcan undo Kahan. - NaN is unequal to everything, itself included: use
std::isnan. Make infinities withnumeric_limits. - Keep money as whole cents or paise in
std::int64_t, and write the rounding rule into the code.
References
- IEEE 754-2019: IEEE Standard for Floating-Point Arithmetic (IEEE)
- Working Draft, Programming Languages: C++ (N4950): [basic.fundamental], [expr.mul], [format.string.std], [charconv.to.chars] (ISO/IEC JTC1/SC22/WG21)
- P1467R9: Extended floating-point types and standard names (ISO/IEC JTC1/SC22/WG21)
- What Every Computer Scientist Should Know About Floating-Point Arithmetic (David Goldberg) (ACM Computing Surveys, reprinted by Oracle)
- Options That Control Optimization (-ffast-math, -fassociative-math) (GNU Project)
- libc++ C++23 Status (LLVM Project)
- Writing ARM64 code for Apple platforms (Apple)
- UndefinedBehaviorSanitizer (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress