1. Introduction
When you first see a double, your hand instinctively writes if (a == b). That’s natural: the brain is used to “equals” meaning “equals”. The problem is that for floating-point numbers == does not compare “mathematical equality” but rather the exact representable bit patterns in memory. Numbers like 0.1 are often stored as the nearest representable binary value, not as the “ideal” 0.1. So in real computations == easily becomes a generator of random surprises.
The most famous example:
#include <iostream>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
std::cout << (a == b) << '\n'; // often 0
}
Why “often”? Because exact behavior depends on representation and printing details, but the general idea is stable: don't rely on exact equality of results from fractional arithmetic.
It's important to note a subtlety: if you compare values that you yourself assigned as identical literals (for example, double x = 1.5; double y = 1.5;), == can be perfectly appropriate. Problems begin where there is arithmetic, loops, formulas, conversions, user input, and so on — that is, almost everywhere the program does something useful.
2. Epsilon comparison: what abs(a - b) < eps means
To deal with rounding error, we introduce a simple “engineering” idea: numbers are considered equal if they are close enough. We express this “closeness” with the number eps (epsilon). Mathematically this is a bit of a compromise, but engineering is happy: now we compare tolerances rather than ideals — like on engineering drawings.
The basic formula looks like this:
\[ |a - b| < eps \]
In code — like this (for now without <cmath>, because the next lecture will cover it):
#include <iostream>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
double diff = a - b;
if (diff < 0.0) diff = -diff; // manual abs
const double eps = 1e-12;
std::cout << (diff < eps) << '\n'; // usually 1
}
The meaning of eps here is important: we are not proving “mathematical equality”, we decide “within the precision of the task these are the same”. In problems about money, coordinates, percentages, physics, geometry — this is almost always what you want.
And yes, this is the moment when a programmer becomes mature: they stop asking “why not equal” and start asking “how close is it”.
3. How to choose eps: an agreement, not a “magic number”
Beginners usually make two extremes when choosing eps: they either pick 0.0000000000000000000001 (to be absolutely sure!), or 0.1 (because “who cares”). In fact eps is part of the contract of your task. It depends on the units you use, the scale of your numbers and what precision actually makes sense for the data.
Imagine measuring distances between cities in kilometers. There eps = 1e-12 is meaningless: no input, map, or GPS works with that. If you compute physics for tiny values, conversely, eps = 1e-3 might be too coarse.
Helpful rough guideline (not a rule, just orientation for beginners):
| Problem context (roughly) | Reasonable order of eps |
|---|---|
| “school math”, simple computations with double | 1e-12 … 1e-9 |
| coordinates/geometry in “ordinary” units | 1e-9 … 1e-6 |
| money/prices (if you really use double, which is debatable) | 1e-6 … 1e-2 (depends on currency/rounding) |
| noisy measurements (sensors) | depends on data noise |
Key principle: eps should be commensurate with the meaning of the task. If you print a number with two decimal places, then eps = 1e-12 looks like someone measuring the temperature of a kettle with a microscope.
Later (at a more advanced level) relative error will be introduced (when eps depends on the magnitude of numbers), but today our goal is to learn basic robustness without overloading theory.
4. Functions for robust comparisons
You want to extract epsilon comparison into a function, because otherwise code quickly becomes a “forest of diffs” and random constants. Let's start developing our mini app of the day: a console “Triangle Inspector” that reads triangle sides and determines its type. Today we need the first building block — the comparison function.
Almost equal: almostEqual and manual abs
First we'll make our own abs for double (purely with basic operators):
double absDouble(double x) {
if (x < 0.0) return -x;
return x;
}
Now — “almost equal”:
bool almostEqual(double a, double b, double eps) {
return absDouble(a - b) < eps;
}
Mini-check in main:
#include <iostream>
int main() {
const double eps = 1e-12;
double a = 0.1 + 0.2;
std::cout << almostEqual(a, 0.3, eps) << '\n'; // 1 (true)
}
Note: we pass eps as a parameter, not hide it inside the function. This is useful because different parts of a program may have different “precision meanings”.
A small remark for the future: the standard library has std::abs, with many overloads for different types. But today we intentionally implement a “manual” variant to avoid depending on the next lecture about <cmath>.
Border comparisons: less/greater with a “margin”
Once you accept the “almost equal” idea, the next question arises: what about conditions like x < limit? On the boundary there can be a situation where it should be exactly limit, but you get limit + 1e-16, and the program suddenly goes into “greater”, even though semantically it’s “the same”. That’s why, when comparison happens near a threshold, we often add a margin via epsilon.
Practical simple rules:
- “strictly less” implement as x < limit - eps
- “strictly greater” implement as x > limit + eps
- “approximately equal” — as abs(x - limit) < eps
You can wrap these into functions (so that if reads like human text):
bool definitelyLess(double a, double b, double eps) {
return a < b - eps;
}
bool definitelyGreater(double a, double b, double eps) {
return a > b + eps;
}
And a mini example:
#include <iostream>
int main() {
const double eps = 1e-12;
double x = 1.0;
double limit = 1.0;
std::cout << definitelyLess(x, limit, eps) << '\n'; // 0
}
This approach is especially useful in branches where behavior changes at a boundary. Otherwise you'll get a “trembling” effect at the threshold: sometimes one branch, sometimes the other — just because of microscopic rounding error.
5. Practical example: detecting a right triangle
Now let's assemble everything into our mini program “Triangle Inspector”. The task is simple: a triangle is right-angled if the Pythagorean theorem holds. But if sides are double, checking with == can fail, especially if sides come from computations (or were entered with decimal points, or resulted from previous formulas).
We need to check:
\[ a^2 + b^2 \approx c^2 \]
First we'll do a small “normalization”: find the maximum side and call it c. Without sorting algorithms — just a couple of ifs, since we're in basic C++.
void sort3(double& a, double& b, double& c) {
if (a > b) { double t = a; a = b; b = t; }
if (b > c) { double t = b; b = c; c = t; }
if (a > b) { double t = a; a = b; b = t; }
}
Right-angle check using epsilon:
bool isRightTriangle(double a, double b, double c, double eps) {
sort3(a, b, c);
return almostEqual(a * a + b * b, c * c, eps);
}
And now main that reads sides and prints the result:
#include <iostream>
int main() {
double a = 0.0, b = 0.0, c = 0.0;
std::cin >> a >> b >> c;
const double eps = 1e-9;
std::cout << isRightTriangle(a, b, c, eps) << '\n'; // 1 for 3 4 5
}
If you input 3 4 5, you expect 1 (true). If you input something like 1 1 1, you expect 0. And importantly: if numbers come from computations (for example, scaling, normalization or divisions), this code will behave predictably rather than “today yes, tomorrow no”.
Why is eps here 1e-9 and not 1e-12? Because we work with squares, and the error can visually “grow”. This is not a strict error analysis, but a practical choice that often makes sense for geometry with user input.
6. What to do with NaN and why everything becomes false
After the lecture on NaN it's important to understand: epsilon comparison is not a “NaN-proof spell”. If NaN appears in your computations somewhere, then a - b will also become NaN, and then absDouble(NaN) stays NaN, and a comparison like NaN < eps yields false. This looks like “nothing works”, but it's actually honest behavior: you cannot compare NaN as a normal number.
At a basic level you can perform a simple diagnostic: a number x is NaN if x != x. Any normal number equals itself, but NaN does not.
For example, you can protect our functions like this (without complicating the design):
bool isNaN(double x) {
return x != x;
}
And do an early exit before important checks:
bool safeAlmostEqual(double a, double b, double eps) {
if (isNaN(a) || isNaN(b)) return false;
return almostEqual(a, b, eps);
}
This is not a “complete protection from all troubles” (we still have Infinity and domain issues), but it already makes program behavior more explainable: if input is bad, we don't pretend everything is fine.
In the next lecture, when we include <cmath>, more standard tools for such checks will appear, but for now we stick to basic material.
7. Common mistakes when using epsilon comparisons
Mistake #1: replace == with an epsilon check once — and assume the problem is solved forever.
Sometimes people do abs(a - b) < 1e-12 in one place, but leave if (x == 0.0) or if (y == limit) elsewhere. As a result the program still “trembles” at boundaries, just a bit less often. If you've accepted the notion of error, comparisons near computations should be done consistently.
Mistake #2: choose eps without relation to the task meaning.
A common scenario: someone saw 1e-9 on the internet and now uses 1e-9 everywhere, including percentages, distances, prices and geometry. eps is part of precision requirements. If you print values with two decimal places, a tolerance of 1e-12 does not make the program smarter — it makes it overconfident.
Mistake #3: forget the absolute value and simply compare a - b < eps.
If a is less than b, the difference is negative, and the condition will almost always be true (for example, -100 < 1e-12 — yes). Therefore you always need the absolute value in epsilon comparison: |a - b|, either manual or from the library.
Mistake #4: use epsilon only for “equals” but ignore boundaries in x < limit.
Even if you compare a and b with epsilon, threshold conditions can break at the border. In real tasks x < limit - eps and x > limit + eps often provide much more stable logic than the “honest” x < limit.
Mistake #5: not realizing that NaN turns comparisons into “everything false”.
If NaN appeared somewhere, then abs(a - b) < eps, and a < b, and a >= b can all start returning false, breaking branches. This is not mysticism or “the computer broke”, it's the specification of NaN. It's useful to at least be able to quickly check x != x and understand: “aha, that's why the condition didn't trigger”.
GO TO FULL VERSION