Relational operators

Relational operators let you compare two values. C++ provides six relational operators:

Operator Symbol Form Operation
Greater than > a > b true if a is greater than b, false otherwise
Less than < a < b true if a is less than b, false otherwise
Greater than or equals >= a >= b true if a is greater than or equal to b, false otherwise
Less than or equals <= a <= b true if a is less than or equal to b, false otherwise
Equality == a == b true if a equals b, false otherwise
Inequality != a != b true if a does not equal b, false otherwise

These operators are intuitive and evaluate to boolean values: true or false.

Here's sample code using relational operators with integers:

#include <iostream>

int main()
{
    std::cout << "Enter a temperature: ";
    int temp1{};
    std::cin >> temp1;

    std::cout << "Enter another temperature: ";
    int temp2{};
    std::cin >> temp2;

    if (temp1 == temp2)
        std::cout << temp1 << " equals " << temp2 << '\n';
    if (temp1 != temp2)
        std::cout << temp1 << " does not equal " << temp2 << '\n';
    if (temp1 > temp2)
        std::cout << temp1 << " is greater than " << temp2 << '\n';
    if (temp1 < temp2)
        std::cout << temp1 << " is less than " << temp2 << '\n';
    if (temp1 >= temp2)
        std::cout << temp1 << " is greater than or equal to " << temp2 << '\n';
    if (temp1 <= temp2)
        std::cout << temp1 << " is less than or equal to " << temp2 << '\n';

    return 0;
}

Sample run:

Enter a temperature: 15
Enter another temperature: 20
15 does not equal 20
15 is less than 20
15 is less than or equal to 20

Relational operators are straightforward with integers.

Boolean conditional values

Conditions in if-statements, conditional operators, and similar contexts evaluate as boolean values by default.

New programmers often write redundant code like:

if (isReady == true) ...

The == true is unnecessary—the condition already produces a boolean value. Write simply:

if (isReady) ...

Similarly, instead of:

if (isReady == false) ...

Write:

if (!isReady) ...
Best Practice
Don't add unnecessary `==` or `!=` to conditions. They make code harder to read without adding value.

Comparison of calculated floating-point values can be problematic

Consider this program:

#include <iostream>

int main()
{
    constexpr double result1{50.0 - 49.95}; // should equal 0.05
    constexpr double result2{5.0 - 4.95};   // should equal 0.05

    if (result1 == result2)
        std::cout << "result1 == result2" << '\n';
    else if (result1 > result2)
        std::cout << "result1 > result2" << '\n';
    else if (result1 < result2)
        std::cout << "result1 < result2" << '\n';

    return 0;
}

Both result1 and result2 should equal 0.05. But this program prints an unexpected result:

result1 > result2

Inspecting in a debugger reveals result1 = 0.050000000000003552 and result2 = 0.049999999999997158. Both are close to 0.05, but result1 is slightly larger while result2 is slightly smaller.

Comparing floating-point values with relational operators is risky. Floating-point values aren't precise—small rounding errors can make operands slightly different than expected, causing relational operators to produce unexpected results.

Related content: We discussed rounding errors in the Floating point numbers lesson.

Floating-point less-than and greater-than

When using <, >, <=, and >= with floating-point values, they usually produce reliable results when operands aren't nearly identical. However, when operands are very close in value, these operators become unreliable. For example, result1 > result2 happens to return true above, but could have returned false if rounding errors went differently.

If getting a wrong answer when operands are similar is acceptable, using these operators may be fine. This is application-specific.

For example, in a game like Space Invaders, determining whether two objects (missile and alien) intersect works fine with these operators. When objects are far apart, the operators return correct answers. When extremely close, you might get either answer—but this would likely appear as a near-miss or near-hit, and the game continues without noticeable issues.

Floating-point equality and inequality

The equality operators (== and !=) are far more problematic. Consider operator==, which returns true only when operands are exactly equal. Even the tiniest rounding error causes two floating-point numbers to be unequal, so operator== risks returning false when you'd expect true. Operator!= has the same problem.

#include <iostream>

int main()
{
    std::cout << std::boolalpha << (0.3 == 0.1 + 0.2); // prints false

    return 0;
}

For this reason, avoid these operators with floating-point operands when values might be calculated.

Warning
Avoid using `operator==` and `operator!=` to compare floating-point values if there's any chance those values were calculated.

One notable exception: It's safe to compare a floating-point literal with a variable initialized with a literal of the same type, provided the literal's significant digits don't exceed the type's minimum precision. Float has a minimum precision of 6 significant digits; double has 15.

Related content: We cover precision for different types in the Floating point numbers lesson.

For example, you may see functions returning floating-point literals (typically 0.0 or sometimes 1.0). In such cases, direct comparison against the same literal of the same type is safe:

if (getDiscount() == 0.0) // okay if getDiscount() returns 0.0 as a literal only
    // no discount

Instead of a literal, you can compare a const or constexpr floating-point variable initialized with a literal:

constexpr double speedOfLight{299792458.0};
if (speedOfLight == 299792458.0) // okay if speedOfLight was initialized with a literal
    // special handling

Comparing floating-point literals of different types is usually unsafe. For example, comparing 9.8f to 9.8 returns false.

Tip
It's safe to compare a floating-point literal with a variable of the same type initialized with a literal of the same type, provided the literal's significant digits don't exceed the type's minimum precision (6 for float, 15 for double). Comparing floating-point literals of different types is generally unsafe.

Comparing floating-point numbers

How can we reasonably compare two floating-point operands for equality?

The most common method uses a function that checks if two numbers are "almost the same." If they're "close enough," we call them equal. The value representing "close enough" is traditionally called epsilon—usually a small positive number like 0.00000001 (or 1e-8).

New developers often try writing their own "close enough" function:

#include <cmath> // for std::abs()

// absEpsilon is an absolute value
bool approximatelyEqualAbs(double a, double b, double absEpsilon)
{
    // if distance between a and b is less than or equal to absEpsilon, they're "close enough"
    return std::abs(a - b) <= absEpsilon;
}

std::abs() (from <cmath>) returns the absolute value of its argument. So std::abs(a - b) <= absEpsilon checks if the distance between a and b is within the "close enough" epsilon. If close enough, the function returns true; otherwise, false.

While this works, it's not ideal. An epsilon of 0.00001 works well for inputs around 1.0, but is too large for inputs around 0.0000001 and too small for inputs like 10000.

Advanced note: If we say any number within 0.00001 of another should be treated as the same:

  • 1 and 1.0001 would differ, but 1 and 1.00001 would be the same—reasonable
  • 0.0000001 and 0.00001 would be the same—not good, as they're two orders of magnitude apart
  • 10000 and 10000.0001 would differ—not good, as they're barely different given their magnitude

This means every call to this function requires picking an epsilon appropriate for the inputs. If we need to scale epsilon proportionally to input magnitude, we might as well have the function do that automatically.

Donald Knuth, a famous computer scientist, suggested this method in "The Art of Computer Programming, Volume II: Seminumerical Algorithms":

#include <algorithm> // for std::max
#include <cmath>     // for std::abs

// Return true if difference between a and b is within relEpsilon percent of the larger of a and b
bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
    return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

Here, epsilon is relative to the operand magnitudes rather than absolute.

Let's examine how this works. On the left side of <=, std::abs(a - b) gives the distance between a and b as a positive number.

On the right side, we calculate the largest "close enough" value we'll accept. The algorithm chooses the larger of a and b (as a rough magnitude indicator), then multiplies by relEpsilon. Here, relEpsilon represents a percentage. To say "close enough" means a and b are within 1% of the larger value, pass relEpsilon of 0.01 (1% = 1/100 = 0.01). Adjust relEpsilon as appropriate (e.g., 0.002 means within 0.2%).

For inequality (!=) instead of equality, call this function and use logical NOT (!) to flip the result:

if (!approximatelyEqualRel(a, b, 0.001))
    std::cout << a << " is not equal to " << b << '\n';

While approximatelyEqualRel() works for most cases, it's imperfect, especially as numbers approach zero:

#include <algorithm>
#include <cmath>
#include <iostream>

bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
    return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

int main()
{
    // a is really close to 1.0, but has rounding errors
    constexpr double a{0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1};

    constexpr double relEps{1e-8};

    std::cout << std::boolalpha;

    // compare a (almost 1.0) to 1.0
    std::cout << approximatelyEqualRel(a, 1.0, relEps) << '\n';

    // compare a-1.0 (almost 0.0) to 0.0
    std::cout << approximatelyEqualRel(a - 1.0, 0.0, relEps) << '\n';

    return 0;
}

This prints:

true
false

The second call didn't work as expected. The math breaks down near zero.

One solution combines absolute epsilon (first approach) with relative epsilon (Knuth's approach):

// Return true if difference between a and b is less than or equal to absEpsilon,
// or within relEpsilon percent of the larger of a and b
bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    // Check if numbers are really close -- needed when comparing near zero
    if (std::abs(a - b) <= absEpsilon)
        return true;

    // Otherwise fall back to Knuth's algorithm
    return approximatelyEqualRel(a, b, relEpsilon);
}

First, we check if a and b are close in absolute terms, handling cases where both are near zero. Set absEpsilon to something very small (e.g., 1e-12). If that fails, fall back to Knuth's algorithm using relative epsilon.

Here's the previous code testing both algorithms:

#include <algorithm>
#include <cmath>
#include <iostream>

bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
    return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    if (std::abs(a - b) <= absEpsilon)
        return true;

    return approximatelyEqualRel(a, b, relEpsilon);
}

int main()
{
    constexpr double a{0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1};

    constexpr double relEps{1e-8};
    constexpr double absEps{1e-12};

    std::cout << std::boolalpha;

    std::cout << approximatelyEqualRel(a, 1.0, relEps) << '\n';         // compare "almost 1.0" to 1.0
    std::cout << approximatelyEqualRel(a - 1.0, 0.0, relEps) << '\n';   // compare "almost 0.0" to 0.0

    std::cout << approximatelyEqualAbsRel(a, 1.0, absEps, relEps) << '\n';       // compare "almost 1.0" to 1.0
    std::cout << approximatelyEqualAbsRel(a - 1.0, 0.0, absEps, relEps) << '\n'; // compare "almost 0.0" to 0.0

    return 0;
}

Output:

true
false
true
true

approximatelyEqualAbsRel() handles small inputs correctly.

Floating-point comparison is difficult, and there's no universal algorithm for every case. However, approximatelyEqualAbsRel() with absEpsilon of 1e-12 and relEpsilon of 1e-8 should handle most cases you'll encounter.

Making the approximatelyEqual functions constexpr (Advanced)

In C++23, these functions can be made constexpr by adding the constexpr keyword:

// C++23 version
#include <algorithm> // for std::max
#include <cmath>     // for std::abs (constexpr in C++23)

constexpr bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
    return (std::abs(a - b) <= (std::max(std::abs(a), std::abs(b)) * relEpsilon));
}

constexpr bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    if (std::abs(a - b) <= absEpsilon)
        return true;

    return approximatelyEqualRel(a, b, relEpsilon);
}

Related content: We cover constexpr functions in the Constexpr functions lesson.

Prior to C++23, calling these constexpr functions in constant expressions fails:

int main()
{
    constexpr double a{0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1};

    constexpr double relEps{1e-8};
    constexpr double absEps{1e-12};

    std::cout << std::boolalpha;

    constexpr bool same{approximatelyEqualAbsRel(a, 1.0, absEps, relEps)}; // compile error: must be initialized by constant expression
    std::cout << same << '\n';

    return 0;
}

This fails because constexpr functions used in constant expressions can't call non-constexpr functions, and std::abs wasn't constexpr until C++23.

The fix is simple—replace std::abs with our own constexpr absolute value implementation:

// C++14/17/20 version
#include <algorithm>
#include <iostream>

// Our own constexpr implementation of std::abs (for use in C++14/17/20)
// In C++23, use std::abs
template <typename T>
constexpr T constAbs(T value)
{
    return (value < 0 ? -value : value);
}

constexpr bool approximatelyEqualRel(double a, double b, double relEpsilon)
{
    return (constAbs(a - b) <= (std::max(constAbs(a), constAbs(b)) * relEpsilon));
}

constexpr bool approximatelyEqualAbsRel(double a, double b, double absEpsilon, double relEpsilon)
{
    if (constAbs(a - b) <= absEpsilon)
        return true;

    return approximatelyEqualRel(a, b, relEpsilon);
}

int main()
{
    constexpr double a{0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1};

    constexpr double relEps{1e-8};
    constexpr double absEps{1e-12};

    std::cout << std::boolalpha;

    constexpr bool same{approximatelyEqualAbsRel(a, 1.0, absEps, relEps)};
    std::cout << same << '\n';

    return 0;
}

Advanced note: The constAbs() version above is a function template, allowing a single definition to handle different value types. We cover function templates in the Function templates lesson.

Summary

  • Relational operators: >, <, >=, <=, ==, !=; compare two values and return boolean results
  • Integer comparisons: Relational operators work reliably and intuitively with integer types
  • Avoid redundant comparisons: Don't write if (isReady == true) or if (isReady == false)—use if (isReady) and if (!isReady) instead
  • Floating-point precision: Floating-point values have rounding errors that make direct comparisons unreliable
  • Less-than/greater-than with floats: <, >, <=, >= usually work when values aren't nearly identical, but become unreliable when operands are very close
  • Equality operators with floats: == and != are highly problematic with calculated floating-point values—avoid them unless comparing literals
  • Safe float literal comparisons: Comparing a float literal with a variable of the same type initialized with the same literal is safe (within precision limits: 6 for float, 15 for double)
  • Epsilon-based comparison: Use "close enough" comparison functions with an epsilon value (e.g., 1e-8) to check if floating-point values are approximately equal
  • Absolute epsilon: Simple approach using fixed epsilon; works poorly for very small or very large values
  • Relative epsilon (Knuth's algorithm): Scales epsilon based on operand magnitudes; works well except near zero
  • Combined approach: approximatelyEqualAbsRel() combines absolute epsilon (for values near zero) with relative epsilon (for other values); recommended with absEpsilon = 1e-12 and relEpsilon = 1e-8
  • constexpr support: In C++23, std::abs is constexpr; in earlier versions, use a custom constAbs() function template for constexpr contexts

Floating-point comparison is one of the most common sources of subtle bugs. Never use == or != with calculated floating-point values. Instead, use epsilon-based comparison functions that treat values as equal when they're "close enough." The combined absolute/relative epsilon approach handles most real-world cases effectively.