Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 double

A 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:

Azvya Education Pvt. Ltd.VLSI Mentor
fp.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0.1 stored as  0.10000000000000000555
0.2 stored as  0.2000000000000000111
0.3 stored as  0.2999999999999999889
0.1 + 0.2      0.30000000000000004441

Read 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::cout << "(0.1 + 0.2) == 0.3 ? "
              << ((a + b) == c ? "true" : "false") << '\n';

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(0.1 + 0.2) == 0.3 ? false

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0.5 + 0.25 == 0.75 ? true

Exactly 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:

ComparisonResultWhy
(0.1 + 0.2) == 0.3falseNone of the three is exactly representable in binary
(0.5 + 0.25) == 0.75trueAll 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:

Azvya Education Pvt. Ltd.VLSI Mentor
fp2.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
float  0.1f = 0.10000000149011611938
double 0.1  = 0.10000000000000000555

Both 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.

Azvya Education Pvt. Ltd.VLSI Mentor
tolerance.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
within tolerance

std::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

  1. float, double and long double are the floating-point types. A plain literal like 1.8 is a double, and double is the sensible default.
  2. Floating point is base 2. Only fractions whose denominators are powers of two have exact finite representations.
  3. This is the same phenomenon as one third in decimal, not a defect.
  4. Measured: 0.1 is stored as 0.10000000000000000555, and 0.3 as 0.2999999999999999889.
  5. (0.1 + 0.2) == 0.3 is false, and the comparison is behaving correctly — the question was wrong.
  6. (0.5 + 0.25) == 0.75 is exactly true, because all three are exactly representable.
  7. Those two results together show the model is "finite binary approximation", not "inaccurate".
  8. Inexact values do not always compare unequal — 0.1 + 0.1 == 0.2 is exactly true. The danger is that you cannot predict which, which is the real reason == on computed values is unsafe.
  9. float and double differ in how many bits they spend, not in whether they can be trusted.
  10. Compare computed values with a tolerance chosen for the magnitudes involved — a fixed absolute tolerance is a starting point, not a universal rule.
  11. Floating point is right for physical quantities, timing, statistics and reference calculations. Integers are right for registers, addresses, counters and protocol fields.
  12. The dividing line is continuous measurement versus exact discrete value — not hardware versus software.
  13. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

  • double is the default; float for memory or an interface that requires it; literals are double unless 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.3 is false; (0.5 + 0.25) == 0.75 is exactly true. One rule predicts both.
  • The model is "finite binary approximation", not "inaccurate" — the behaviour is deterministic and specified.
  • float versus double is 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.