Is this design of operator== correct?

1 day ago 3
ARTICLE AD BOX

What is wrong with your design:

Static Type Dependency: The result of the comparison changes based on the pointer/reference type (static type) rather than the actual object (dynamic type). This makes equality non-substitutable and breaks basic logic.

Meaningless Base Equality: A::operator== always returns true. This means any two objects derived from A will report being equal when compared through a base reference, regardless of their actual data.

Member Logic Slicing: C::operator== ignores the data inherited from B (the integer i). This allows objects with different base states to be incorrectly flagged as equal.

Template Hijacking: The template operator== is too broad and can steal (hijack) overload resolution. It makes cross-type comparisons (like B == C) compile but forces them to always be false, leading to surprising and inconsistent outcomes.

Broken Consistency: Comparisons through A& use the "always true" base operator, while direct derived comparisons use the specific member operators. This mismatch creates a bug where the same objects are both equal and not equal depending on the code context.

Equivalence Relation Violation: The design fails to provide a stable, consistent comparison. This violates the mathematical expectations (Symmetry, Transitivity) required for standard algorithms and containers to function correctly.

How to do it better?

That depends if you want to provide cross-type equality.

Safest - do not and generate compile error:

struct A { virtual ~A() = default; friend bool operator==(const A& lhs, const A& rhs) noexcept { if (typeid(lhs) != typeid(rhs)) { return false; } return lhs.equals_same_type(rhs); } friend bool operator!=(const A& lhs, const A& rhs) noexcept { return !(lhs == rhs); } protected: virtual bool equals_same_type(const A& /*other*/) const noexcept { return true; } }; struct B : A { int i = 0; explicit B(int _i = 0) : i(_i) {} protected: bool equals_same_type(const A& other) const noexcept override { const B& o = static_cast<const B&>(other); return i == o.i; } }; struct C : B { float f = 0.0f; explicit C(float _f, int _i = 0) : B(_i), f(_f) {} protected: bool equals_same_type(const A& other) const noexcept override { const C& o = static_cast<const C&>(other); return B::equals_same_type(other) && (std::fabs(f - o.f) < 0.1f); } }; /** * Disable cross-type equality at compile time. * * This makes expressions like B{} == C{} ill-formed (compile error), * while still allowing comparisons through A& / A*: * A& a = b; A& c = cobj; (a == c) is well-formed and returns false. */ template <std::derived_from<A> T, std::derived_from<A> U> requires (!std::same_as<T, U>) bool operator==(const T&, const U&) = delete;

Then there are many version possble depending of what you want to archive:

Exmaple if you wasnt to comare via A&/A*

struct A { virtual ~A() = default; friend bool operator==(const A& lhs, const A& rhs) noexcept { if (typeid(lhs) != typeid(rhs)) { return false; /* Different dynamic types -> not equal */ } return lhs.equals_same_type(rhs); } friend bool operator!=(const A& lhs, const A& rhs) noexcept { return !(lhs == rhs); } protected: virtual bool equals_same_type(const A& /*other*/) const noexcept { return true; /* A has no state */ } }; struct B : A { int i = 0; explicit B(int _i = 0) : i(_i) {} protected: bool equals_same_type(const A& other) const noexcept override { const B& o = static_cast<const B&>(other); return i == o.i; } }; struct C : B { float f = 0.0f; static constexpr float kEps = 0.1f; explicit C(float _f, int _i = 0) : B(_i), f(_f) {} protected: bool equals_same_type(const A& other) const noexcept override { const C& o = static_cast<const C&>(other); /* Compare base (B) state and then C-specific state. */ if (!B::equals_same_type(other)) { return false; } /* Make NaNs never equal (common, predictable policy). */ if (!std::isfinite(f) || !std::isfinite(o.f)) { return false; } return std::fabs(f - o.f) < kEps; } }; inline bool equal_ptr(const A* lhs, const A* rhs) noexcept { if (lhs == rhs) { return true; /* Same pointer (including both null) */ } if ((lhs == nullptr) || (rhs == nullptr)) { return false; } return (*lhs == *rhs); }
Read Entire Article