Question
In C++, what is an efficient and correct way to compare two double values or two float values while accounting for floating-point precision loss?
A direct comparison like this is often unreliable:
bool CompareDoubles1(double a, double b)
{
return a == b;
}
A tolerance-based approach is more appropriate:
bool CompareDoubles2(double a, double b)
{
double diff = a - b;
return (diff < EPSILON) && (-diff < EPSILON);
}
However, this may seem inefficient or incomplete. Is there a better or smarter way to compare floating-point numbers?
Short Answer
By the end of this page, you will understand why float and double values cannot always be compared safely with ==, how tolerance-based comparison works, when to use absolute vs relative tolerance, and how to write practical floating-point comparison functions in C++.
Concept
Floating-point numbers such as float and double are stored in binary, not decimal. Because of that, many decimal values cannot be represented exactly.
For example, a value like 0.1 looks simple in decimal, but its binary representation is only an approximation. That means calculations can produce tiny rounding differences.
double a = 0.1 + 0.2;
double b = 0.3;
// Often false
bool same = (a == b);
Even though the math looks equal, the stored values may differ by a very small amount.
This matters because real programs often use floating-point numbers for:
- scientific calculations
- graphics and game development
- financial approximations
- measurements and sensor data
- percentages and statistics
A good comparison usually checks whether two numbers are close enough rather than exactly equal.
There are two common ideas:
- Absolute tolerance: useful when numbers are near zero
- Relative tolerance: useful when numbers may be very large or very small
A robust comparison often combines both.
Important note: exact comparison with == is not always wrong. It is acceptable when:
- you intentionally want exact bit-pattern equality
- values come from the same calculation path
- comparing with
0.0in some controlled cases - checking special values like
infinityafter known operations
But for most numeric results, tolerance-based comparison is the safer approach.
Mental Model
Think of floating-point numbers like measurements made with a ruler.
If one object measures 10.0000001 cm and another measures 10.0000002 cm, they are effectively the same for many purposes. A strict == comparison acts like saying those objects are different because the ruler showed slightly different marks.
A tolerance-based comparison is like saying:
- "If the difference is tiny enough, treat them as equal."
Another way to picture it:
==asks: Are these numbers identical?- epsilon comparison asks: Are these numbers close enough to be considered the same?
The tricky part is deciding what “close enough” means. A difference of 0.0001 may be tiny for 1000000.0, but huge for 0.00001. That is why relative tolerance is often needed.
Syntax and Examples
The simplest safe pattern uses std::abs and a tolerance value.
#include <cmath>
bool nearlyEqual(double a, double b, double epsilon = 1e-9)
{
return std::abs(a - b) < epsilon;
}
This works well when the numbers are expected to be near the same scale and especially near zero.
Example
#include <iostream>
#include <cmath>
bool nearlyEqual(double a, double b, double epsilon = 1e-9)
{
return std::abs(a - b) < epsilon;
}
int main()
{
double a = 0.1 + 0.2;
double b = 0.3;
std::cout << std::boolalpha;
std::cout << (a == b) << "\n";
std::cout << nearlyEqual(a, b) << "\n";
}
Typical output:
false
true
Better: combine absolute and relative tolerance
A more practical comparison handles both small and large values.
#include <cmath>
#include <algorithm>
bool nearlyEqual(double a, double b, double absEpsilon = 1e-12, double relEpsilon = 1e-9)
{
double diff = std::abs(a - b);
if (diff <= absEpsilon)
return true;
return diff <= std::max(std::abs(a), std::abs(b)) * relEpsilon;
}
Why this version is better
absEpsilonhelps when values are close to zerorelEpsilonscales the allowed difference for large values- it works more reliably across different magnitudes
Float version
bool nearlyEqual(float a, float b, float absEpsilon = 1e-6f, float relEpsilon = 1e-5f)
{
float diff = std::fabs(a - b);
if (diff <= absEpsilon)
return true;
return diff <= std::max(std::fabs(a), std::fabs(b)) * relEpsilon;
}
The exact tolerance values depend on your application. There is no universal epsilon that fits every problem.
Step by Step Execution
Consider this example:
#include <cmath>
#include <algorithm>
bool nearlyEqual(double a, double b, double absEpsilon = 1e-12, double relEpsilon = 1e-9)
{
double diff = std::abs(a - b);
if (diff <= absEpsilon)
return true;
return diff <= std::max(std::abs(a), std::abs(b)) * relEpsilon;
}
Now call it like this:
double a = 1000000.0;
double b = 1000000.0001;
bool result = nearlyEqual(a, b);
Step by step
-
diff = std::abs(a - b)a - bis about-0.0001- absolute difference becomes
0.0001
-
Check absolute tolerance:
0.0001 <= 1e-12is false- so the values are not close enough by absolute tolerance alone
-
Compute relative scale:
std::max(std::abs(a), std::abs(b))is about1000000.0001
-
Multiply by relative tolerance:
1000000.0001 * 1e-9is about0.001
-
Final comparison:
0.0001 <= 0.001is true
-
Function returns
true
This makes sense because for numbers around one million, a difference of 0.0001 is extremely small.
Now compare numbers near zero:
double x = 0.0000000000001;
double y = 0.0;
Here, relative comparison alone can behave poorly because both numbers are tiny. That is why the absolute tolerance check comes first.
Real World Use Cases
Floating-point comparison appears in many practical situations.
Graphics and games
Positions, rotations, and physics values are often stored as float.
if (nearlyEqual(playerX, targetX, 1e-4f, 1e-4f))
{
// Player has effectively reached the target position
}
Scientific and engineering calculations
Measurements and simulations produce approximate results.
if (nearlyEqual(simulatedValue, expectedValue, 1e-10, 1e-8))
{
// Result is within acceptable error
}
Data processing
When reading decimal values from files, APIs, or sensors, minor representation differences are common.
if (nearlyEqual(temperatureReading, 25.0, 1e-6, 1e-6))
{
// Treat as 25 degrees
}
Testing numeric code
Unit tests for mathematical functions should usually avoid strict equality.
ASSERT_TRUE(nearlyEqual(result, 3.1415926535, 1e-12, 1e-9));
Finance with caution
Floating-point values are usually not ideal for exact money calculations, but if they are used for approximations, tolerance-based comparison is necessary.
if (nearlyEqual(taxEstimate, expectedTax, 1e-6, 1e-6))
{
// Acceptable estimate
}
Real Codebase Usage
In real codebases, developers usually wrap floating-point comparison in a helper function instead of repeating the logic everywhere.
Common pattern: utility function
namespace math_utils
{
bool nearlyEqual(double a, double b, double absEpsilon = 1e-12, double relEpsilon = 1e-9)
{
double diff = std::abs(a - b);
if (diff <= absEpsilon)
return true;
return diff <= std::max(std::abs(a), std::abs(b)) * relEpsilon;
}
}
This improves:
- readability
- consistency
- easier tuning of tolerances
Guard clauses
Developers often use guard clauses before expensive numeric work.
if (nearlyEqual(length, 0.0))
return;
Validation
if (!nearlyEqual(totalPercentage, 100.0, 1e-9, 1e-9))
{
throw std::runtime_error("Percentages must add up to 100");
}
Testing helpers
Many codebases create custom assertions for numeric tests.
void assertNearlyEqual(double actual, double expected)
{
if (!nearlyEqual(actual, expected))
{
throw std::runtime_error("Values are not close enough");
}
}
Domain-specific tolerances
A physics engine, graphics tool, and statistics library may all use different tolerances. Real projects usually choose tolerances based on domain requirements, not on a single hardcoded global epsilon.
Performance note
The comparison itself is already very cheap. In most applications, the cost of a few abs and max operations is negligible compared with the benefit of correctness. The smarter comparison is usually the one that gives reliable results, not the one with the fewest operations.
Common Mistakes
Here are common beginner mistakes when comparing floating-point numbers.
1. Using == for computed results
Broken example:
double a = 0.1 + 0.2;
double b = 0.3;
if (a == b)
{
// May never run
}
Better:
if (nearlyEqual(a, b))
{
// Safer
}
2. Using only absolute tolerance for all cases
Broken example:
bool nearlyEqual(double a, double b)
{
return std::abs(a - b) < 1e-9;
}
Problem:
- works for some values
- fails when numbers are very large
Use combined absolute and relative tolerance instead.
3. Choosing a tolerance that is too large
bool nearlyEqual(double a, double b)
{
return std::abs(a - b) < 0.1;
}
This may mark clearly different values as equal.
Avoid this by choosing tolerances based on the problem domain.
4. Choosing a tolerance that is too small
bool nearlyEqual(double a, double b)
{
return std::abs(a - b) < 1e-30;
}
This can be nearly as strict as == for normal applications.
5. Forgetting values near zero
Relative comparison alone can break near zero.
Broken idea:
bool nearlyEqual(double a, double b)
{
double diff = std::abs(a - b);
return diff <= std::max(std::abs(a), std::abs(b)) * 1e-9;
}
If both values are near 0, the right side may become too small. Add an absolute tolerance check first.
6. Assuming std::numeric_limits<double>::epsilon() solves everything
Machine epsilon is the spacing near 1.0, not a universal comparison tolerance for all values.
#include <limits>
double eps = std::numeric_limits<double>::epsilon();
This value is useful, but not automatically the right threshold for your application.
7. Ignoring NaN
NaN is never equal to anything, not even itself.
double x = std::nan("1");
bool a = (x == x); // false
If NaN is possible in your program, handle it explicitly.
Comparisons
Here is a practical comparison of common approaches.
| Approach | Example | Good for | Weakness |
|---|---|---|---|
| Exact equality | a == b | exact checks, controlled values | unreliable for computed floating-point results |
| Absolute tolerance only | abs(a - b) < eps | values near zero, fixed small scales | poor for very large values |
| Relative tolerance only | diff <= max(abs(a), abs(b)) * eps | large or varying magnitudes | weak near zero |
| Absolute + relative tolerance | combine both checks | most general-purpose numeric comparisons | requires choosing tolerances |
float vs double
| Type | Precision | Typical use |
|---|---|---|
float | lower precision | graphics, large arrays, performance-sensitive cases |
double | higher precision | general calculations, scientific code, most numeric applications |
epsilon vs application tolerance
| Term | Meaning |
|---|---|
| machine epsilon | the gap between 1.0 and the next representable value |
| application tolerance | the acceptable error for your specific problem |
In real programs, application tolerance matters more than machine epsilon alone.
Cheat Sheet
#include <cmath>
#include <algorithm>
bool nearlyEqual(double a, double b, double absEpsilon = 1e-12, double relEpsilon = 1e-9)
{
double diff = std::abs(a - b);
if (diff <= absEpsilon)
return true;
return diff <= std::max(std::abs(a), std::abs(b)) * relEpsilon;
}
Quick rules
- Do not use
==for most computedfloatordoubleresults - Use
std::abs(a - b)to measure the difference - Use absolute tolerance for values near zero
- Use relative tolerance for large magnitudes
- Combine both for general-purpose comparison
- Pick tolerances based on your problem, not blindly
std::numeric_limits<T>::epsilon()is not a universal answer- Handle
NaNexplicitly if it may appear
Useful defaults
For double:
absEpsilon = 1e-12
relEpsilon = 1e-9
For float:
absEpsilon = 1e-6f
relEpsilon = 1e-5f
Near-zero pattern
if (std::abs(x) < 1e-12)
{
// Treat as zero
}
Bad pattern
return a == b;
Better pattern
return nearlyEqual(a, b);
FAQ
Should I ever use == with doubles in C++?
Yes, in controlled situations such as exact constants, special-case logic, or when exact identity is intended. For computed results, use tolerance-based comparison.
Is std::numeric_limits<double>::epsilon() the right tolerance?
Not usually by itself. It describes floating-point spacing near 1.0, not the acceptable error for all values or all applications.
What tolerance should I use for comparing floats?
It depends on the domain. A common starting point is around 1e-6f absolute and 1e-5f relative, then adjust based on your required accuracy.
Why do I need both absolute and relative tolerance?
Absolute tolerance works well near zero. Relative tolerance works well for large values. Using both makes the comparison more reliable across different scales.
Is the tolerance-based method slower than ==?
Yes, slightly, but the difference is tiny in most programs. Correctness is usually much more important than saving a few arithmetic operations.
How do I compare values to zero?
Usually with an absolute tolerance:
std::abs(x) < 1e-12
What happens if one value is NaN?
Comparisons involving NaN are special. NaN == NaN is false, and tolerance-based checks should handle NaN explicitly if it may occur.
Should I use float or double?
Use double by default for general numeric work unless you have a specific reason to use float, such as memory usage or graphics APIs.
Mini Project
Description
Build a small C++ program that compares pairs of floating-point numbers and reports whether they are approximately equal. This project demonstrates why strict equality is unreliable and how a reusable comparison helper improves correctness.
Goal
Create a console program that compares several double values using both == and a safe tolerance-based function.
Requirements
- Write a
nearlyEqualfunction fordoublevalues using absolute and relative tolerance. - Compare at least three pairs of values, including
0.1 + 0.2vs0.3. - Print the result of both exact equality and approximate equality.
- Include one example with large numbers and one example near zero.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.