Same, same, but different: when different values compare equal

Contents

In C++, there are a few ways how values that we would consider different compare equal. A short overview.

Here, with “compare equal” I mean, that the expression a == b for two different values a and b would be true. And with “different” I mean that inspecting the value, e.g. with a debugger or by printing it on the console, would show a difference.

User-defined types

To be able to compare instances of classes and structs, we have to define the comparison operator ourselves. This, in turn, makes the topic of different values comparing equal rather boring. After all, we can just define the comparison operator to always return true for one of our classes.

Other user-defined types are enums. We can not directly compare scoped enums of different types (aka. enum classes). If we compare enums of the same type or different classic C enums, we get the result of comparing the underlying integral value. There is nothing exciting going on – unless we forget that consecutive enumerators are given increasing values by the compiler if we do not define them differently:

enum class E {
   FIRST,
   SECOND = -1,
   THIRD,
   FOURTH,
   //...
};

static_assert(E::FIRST == E::THIRD);

Here, FIRST gets automatically assigned the value 0, and, after we explicitly set SECOND to -1, THIRD is 0 again, FOURTH is 1 and so on. However, we just have two different names for the same value here, not different values. Inspecting two objects of type E with the values FIRST and THIRD would give us the exact same result, making them indistinguishable.

Built-in types

At first sight, we can say that comparing two objects of the same built-in type will be boring. They’d have to have the same value to compare equal, and only different values would not compare equal. Except that’s not true!

Different zeroes compare equal

When we deal with floating point types, we have exceptions to these rules. The C++ standard does not specify how floating point types are represented internally, but many platforms use IEEE 754 floating point representation.

In IEEE 754, there are two distinguishable values for zero: positive and negative zero. The bitwise representation is different, and we will see different values when debugging or printing them. However, the two compare equal. On the other hand, floating points contain the value NaN (not a number). And when we compare a variable with such a value with itself, they don’t compare equal.

static_assert(-0.0 == 0.0);

int main() {
  //prints "0 -0"
  std::cout << 0.0 << ' ' << -0.0 << '\n';
}

constexpr double nan = std::numeric_limits<double>::quiet_NaN();
static_assert(nan != nan);

Different integral values that compare equal

You’ll hopefully agree with me that a value of type unsigned int cannot be negative. If we have e.g. a variable u of type unsigned int and the comparison u >= 0, this will always be true. Compilers may even warn about it, and optimizers may use it to optimize our code.

Nevertheless, there may be values for u such that u == -1 return true. The reason is that we’re comparing an unsigned int with an int here, and the compiler has to convert one to the other type. In this case, two’s complement is used to convert the int to unsigned int, which will give the largest possible unsigned int:

static_assert(std::numeric_limits<unsigned int>::max() == -1);

Usually, this makes a lot of sense at the bit representation level: If the int is already represented as two’s complement, with a leading sign bit, then these two values have the exact same bit representation. unsigned int has to use two’s complement according to the standard. However, the bit representation for the int is implementation-defined and might be something different entirely.

Different pointer values that compare equal

Have a look at this piece of code:

struct A { unsigned int i = 1; };
struct B { unsigned int j = 2; };
struct C : A, B {};

constexpr static C c;
constexpr B const* pb = &c;
constexpr C const* pc = &c;

static_assert(pb == pc);
static_assert((void*)pb != (void*)pc);

The last two lines are interesting: when we directly compare pb and pc, they are equal. The constexpr and const keywords do not play any role in that, they are only needed to make the comparisons a constant expression for the static_assert. When we cast them to void* first, i.e. compare the exact memory locations they point to, they are not. The latter can also be shown by simply printing the pointers:

#include <iostream>
int main() {
    std::cout << pc << '\n' << pb << '\n';
}

The output will be something like this:

0x400d38
0x400d3c

So, what is going on here? The clue is that, again, we have two different types that can not be compared directly. Therefore, the compiler has to convert one into the other. Since C inherits B, a C* is convertible to a B* (and C const* to B const*). We already used that fact when we initialized pb, so it is not a big surprise that they compare equal.

But why do they have different values? For this, we have to look at the memory layout of c. Since it inherits first from A, and then from B, the first bytes are needed to store the A subobject and its member i. The B subobject with its j member comes after that and therefore can not have the same actual address as c.

graphic of pb and pc in relation

This is different if either A or B do not have any nonstatic data members. The compiler may optimize away empty base classes, and then pb, pc and a pointer to the A subobject of c would contain the same address.

Previous Post
Next Post
Posted in

2 Comments


  1. Nitpick, but I don’t quite agree that ‘u > 0’ is always true for an unsigned variable ‘u’. If you change it to ‘u >= 0’, thin I agree that it must always be true.

    Reply

    1. Not a nitpick, but a nicely spotted error, thanks! Fixed 🙂

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *