C++ Part I · Module 3 · Types, Variables, and Initialization
Floating-Point Types and What They Cannot Represent
A lot of engineering work involves quantities that are not whole numbers: a supply voltage, a measured propagation delay, a throughput figure, an average over a million samples. C++ represents those with floating-point types, and they have a reputation for being unreliable that is not quite deserved. They are not inaccurate in the sense of being unpredictable. They are a finite binary approximation of the real numbers, and everything surprising about them follows from one fact that is easy to state and easy to check: most of the decimal fractions people write down have no exact representation in binary, in the same way that one third has no exact representation in decimal. Once you see what is actually stored in the object when you write 0.1, the famous surprises stop being surprises and become predictable consequences — including the one that matters most in practice, which is why comparing two of these values with == is a bug pattern rather than a matter of taste.
Foundation16 min readC++Part IFundamentalsFloating Point
Part I · Module 3 · Chapter 3.5 · Floating-Point Types and What They Cannot Represent
1. The Types
C++ gives you three:
float sample_value = 1.8f; // single precision
double supply_voltage = 1.8; // double precision — the default
long double extended = 1.8L; // at least as precise as doubleA literal like 1.8 is a double. The f suffix makes it a float, and L makes it a long double.
Use double unless you have a specific reason not to. It is the default for a reason: more precision for a size that is unproblematic on modern hardware. Reach for float when you have a large array of values and memory matters, or when a hardware interface specifies it.
Measured on the machine used for this chapter: sizeof(float) is 4 and sizeof(double) is 8. Per Chapter 3.2, treat those as this platform's choices — though in practice float and double almost universally mean the IEEE-754 32-bit and 64-bit formats.
2. The Problem, Stated Without C++
Before any code, a fact you already accept.
Write one third in decimal: 0.333333… It never terminates. Decimal cannot represent one third exactly, because 3 is not a factor of 10. Stop anywhere and you have an approximation.
Nobody considers this a flaw in decimal. It is a consequence of writing numbers in base 10.
Floating-point numbers are written in base 2, and exactly the same thing happens — just to different numbers. In base 2 the only fractions that terminate are those whose denominators are powers of two: a half, a quarter, an eighth, three quarters.
One tenth is not one of them. 0.1 in binary is a repeating pattern that never terminates, exactly as one third is in decimal. A double has a finite number of bits, so it stores the closest value it can.
That is the entire mechanism. Everything in this chapter follows from it.
3. What Is Actually Stored
You do not have to take that on faith. Ask the program to print more digits than usual:
#include <iomanip>
#include <iostream>
int main()
{
const double a = 0.1;
const double b = 0.2;
const double c = 0.3;
std::cout << std::setprecision(20);
std::cout << "0.1 stored as " << a << '\n';
std::cout << "0.2 stored as " << b << '\n';
std::cout << "0.3 stored as " << c << '\n';
std::cout << "0.1 + 0.2 " << a + b << '\n';
return 0;
}Expected output:
0.1 stored as 0.10000000000000000555
0.2 stored as 0.2000000000000000111
0.3 stored as 0.2999999999999999889
0.1 + 0.2 0.30000000000000004441Read those four lines carefully, because they contain the whole lesson.
None of the three constants is stored as the number you wrote. Each is stored as the nearest value the format can represent. 0.1 is slightly over; 0.3 is slightly under.
The sum is not equal to the stored 0.3. Adding the stored 0.1 to the stored 0.2 gives 0.30000000000000004441, while the stored 0.3 is 0.2999999999999999889. These are different values, and the difference is real rather than a display artifact.
std::setprecision asks the stream for more digits. It is a formatting control and Chapter 23.2 owns the subject; it appears here because it is the only way to see what is in the object.
4. The Consequence: == Is a Bug Pattern
std::cout << "(0.1 + 0.2) == 0.3 ? "
<< ((a + b) == c ? "true" : "false") << '\n';Expected output:
(0.1 + 0.2) == 0.3 ? falseThe comparison is correct. It compared two values, they differ, it said so. The mistake was asking the question.
Comparing two computed floating-point values with
==asks whether they are bit-for-bit identical. Almost no interesting calculation guarantees that.
Two different routes to the same mathematical answer produce two slightly different stored values, and == reports the difference faithfully.
5. The Control Case: When Equality Is Exact
This is the part most treatments leave out, and without it the model is wrong.
const double half = 0.5;
const double quarter = 0.25;
std::cout << "0.5 + 0.25 == 0.75 ? "
<< ((half + quarter) == 0.75 ? "true" : "false") << '\n';Expected output:
0.5 + 0.25 == 0.75 ? trueExactly true. No approximation, no tolerance, no luck.
The reason is §2's rule applied in the other direction. A half, a quarter and three quarters are all sums of negative powers of two, so all three have exact finite binary representations. The arithmetic is exact, and so is the comparison.
Now put the two results side by side:
| Comparison | Result | Why |
|---|---|---|
(0.1 + 0.2) == 0.3 | false | None of the three is exactly representable in binary |
(0.5 + 0.25) == 0.75 | true | All three are exactly representable in binary |
Same language, same type, same operator, opposite outcomes — and both predicted by one rule.
One refinement, because the rule is easy to over-apply. Inexact representation can make a comparison fail; it does not always. 0.1 + 0.1 == 0.2 is exactly true, even though neither 0.1 nor 0.2 is exactly representable — because doubling a binary floating-point value is itself exact, so the stored 0.1 doubled lands precisely on the stored 0.2.
So the honest statement is not "inexact values always compare unequal". It is:
Once a value is inexact, whether a later comparison succeeds depends on the particular arithmetic. You can no longer tell by looking.
That is the real argument against == on computed values. Not that it always fails — that you cannot predict when it will.
6. float Versus double
The same value in both:
#include <iomanip>
#include <iostream>
int main()
{
const float f = 0.1f;
const double d = 0.1;
std::cout << std::setprecision(20);
std::cout << "float 0.1f = " << f << '\n';
std::cout << "double 0.1 = " << d << '\n';
return 0;
}Expected output:
float 0.1f = 0.10000000149011611938
double 0.1 = 0.10000000000000000555Both are approximations of 0.1. The double is closer, because it has more bits to spend.
So precision is a question of how many bits, not of whether the type is trustworthy. Neither is wrong; one is nearer. This is also why double is the sensible default — the extra precision is usually free in practice, and the cases where float wins are specific ones you will recognise.
7. Comparing Properly
If == is the wrong question, what is the right one?
Compare with a tolerance: ask whether two values are close enough for the purpose, rather than identical.
#include <cmath>
#include <iostream>
int main()
{
const double measured = 0.1 + 0.2;
const double expected = 0.3;
const double tolerance = 1e-9;
if (std::fabs(measured - expected) < tolerance) {
std::cout << "within tolerance\n";
} else {
std::cout << "outside tolerance\n";
}
return 0;
}Expected output:
within tolerancestd::fabs from <cmath> gives the absolute value, so the test is "is the gap smaller than the tolerance" regardless of direction.
Two honest caveats, because a fixed tolerance is a starting point rather than a universal answer:
The tolerance depends on the problem. 1e-9 is sensible for values near 1. For values near a million it is far too strict, and for values near 10⁻¹⁵ it is meaninglessly loose. Choosing it is an engineering decision about what difference matters, not a constant to copy.
A fixed tolerance is an absolute test. For values spread over a wide range, a relative comparison — a difference as a fraction of the size of the values — is usually better. The principle to carry is this: decide what "equal enough" means for your quantity, then test that.
8. Where Floating Point Belongs in This Field
It is tempting to summarise as "hardware uses integers, software uses floating point". That is false and it will mislead you.
Floating point is the right tool for:
- Physical quantities. A supply voltage of 1.8 V, a temperature, a current. These are real-world measurements and are not whole numbers.
- Timing and delay analysis. A propagation delay of 0.37 ns, a setup margin, a clock period.
- Performance and statistical results. An average latency over a million transactions, a hit rate, a throughput figure, a standard deviation. Chapter 1.5 §4's architectural models live here.
- Reference calculations where the specification itself is defined in real-number arithmetic — filter coefficients, signal-processing results.
Integers are the right tool for:
- Register values and bit patterns. A register is a pattern of bits, not a quantity. Chapter 3.4.
- Addresses. An address is an exact whole number and must stay exact.
- Counters. Transactions, cycles, errors, packets.
- Protocol fields. A length field is a specified-width integer.
The dividing line is not hardware versus software. It is whether the quantity is a measurement of something continuous, or an exact discrete value. A cycle count is discrete and must be exact; a delay is continuous and is always an approximation anyway, including in the measurement that produced it.
One warning that follows directly: never accumulate a count in a floating-point type. A double can hold enormous values, but once it is large enough, adding 1 to it can leave it unchanged — the gap between representable values has grown past 1. Counters are integers.
9. Mistakes Worth Making Once
Comparing computed values with ==. The two most famous constants in programming are 0.1 + 0.2 and 0.3, and they are not equal.
Believing floating point is random or unreliable. It is deterministic and specified. 0.5 + 0.25 == 0.75 is exactly true, every time.
Copying a tolerance without thinking. 1e-9 is right near 1 and wrong near 10⁶. The tolerance is a statement about what difference matters for your quantity.
Using a floating-point type for a counter. Past a certain magnitude, adding 1 changes nothing. Counters are integers.
Using a floating-point type for a register value or an address. Those are exact discrete values; the type is the wrong shape for them regardless of range.
Assuming float is "the fast one" and reaching for it by default. On modern hardware double is usually not slower for scalar work, and it is more precise. Choose float for a reason — memory for large arrays, or an interface that requires it.
Expecting a printed value to be the stored value. By default a stream prints a rounded version. 0.1 printed normally looks like 0.1; setprecision(20) showed what is actually there.
10. What You Should Be Able to Say Now
float,doubleandlong doubleare the floating-point types. A plain literal like1.8is adouble, anddoubleis the sensible default.- Floating point is base 2. Only fractions whose denominators are powers of two have exact finite representations.
- This is the same phenomenon as one third in decimal, not a defect.
- Measured: 0.1 is stored as 0.10000000000000000555, and 0.3 as 0.2999999999999999889.
(0.1 + 0.2) == 0.3is false, and the comparison is behaving correctly — the question was wrong.(0.5 + 0.25) == 0.75is exactly true, because all three are exactly representable.- Those two results together show the model is "finite binary approximation", not "inaccurate".
- Inexact values do not always compare unequal —
0.1 + 0.1 == 0.2is exactly true. The danger is that you cannot predict which, which is the real reason==on computed values is unsafe. floatanddoublediffer in how many bits they spend, not in whether they can be trusted.- Compare computed values with a tolerance chosen for the magnitudes involved — a fixed absolute tolerance is a starting point, not a universal rule.
- Floating point is right for physical quantities, timing, statistics and reference calculations. Integers are right for registers, addresses, counters and protocol fields.
- The dividing line is continuous measurement versus exact discrete value — not hardware versus software.
- Never accumulate a counter in a floating-point type.
11. Exercises
Exercise 1 — predict exactness. For each, say whether the comparison is exactly true and why: (a) 0.5 + 0.5 == 1.0 (b) 0.1 + 0.1 == 0.2 (c) 0.25 + 0.125 == 0.375 (d) 0.2 + 0.4 == 0.6. One of these involves values that are not exactly representable and is still exactly true — identify it and explain why, using §5's refinement.
Exercise 2 — explain the mechanism. In two sentences, explain to a colleague why 0.1 cannot be stored exactly, without using the word "inaccurate". Use the decimal analogy.
Exercise 3 — fix the test. Rewrite so it behaves sensibly, and say how you chose the tolerance:
if (measured_delay == 0.37) { /* ... */ }Exercise 4 — choose the type. For each, pick floating point or integer and justify in one phrase: (a) a 32-bit status register; (b) an average latency over 10⁶ transactions; (c) the number of transactions; (d) a supply voltage; (e) a memory address; (f) a measured setup margin in nanoseconds.
Exercise 5 — the counter that stops counting. Explain why accumulating a transaction count in a double eventually stops increasing, with reference to how the gap between representable values changes.
Exercise 6 — why the control case matters. Someone says all floating-point comparisons are unreliable. Use the 0.5 + 0.25 result to show the claim is too strong, and state the accurate version.
Exercise 7 — printed versus stored. A value prints as 0.1 normally and as 0.10000000000000000555 with setprecision(20). Explain which one is "the value in the object" and what the default output is doing.
12. Summary
doubleis the default;floatfor memory or an interface that requires it; literals aredoubleunless suffixed.- Floating point is base 2, so only fractions over powers of two are exact — the same phenomenon as one third in decimal.
- Measured: 0.1 is stored as 0.10000000000000000555 and 0.3 as 0.2999999999999999889.
(0.1 + 0.2) == 0.3is false;(0.5 + 0.25) == 0.75is exactly true. One rule predicts both.- The model is "finite binary approximation", not "inaccurate" — the behaviour is deterministic and specified.
floatversusdoubleis a bit-count question, not a trustworthiness question.- Compare with a tolerance you chose for the magnitudes involved, not a copied constant.
- Floating point: physical quantities, timing, statistics, reference calculations. Integers: registers, addresses, counters, protocol fields.
- The line is continuous versus exact discrete, not hardware versus software.
- Counters are never floating point.
Two groups of fundamental types remain, and they are the two that are least like the others. One holds a single logical answer — true or false — and is not simply a small integer, though it converts to one readily enough to cause confusion. The other is the type C++ uses for characters, which turns out to be an integer type that happens to be used for text, and which comes in three distinct flavours whose differences matter more than they look. Chapter 3.6 covers both, including one property of plain char that varies between platforms and that catches people who assume it does not.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
