Comparisons and Operator <=>
- Operator != with !(a==b)
- If above doesn't work, change the order of the operands. !(b==a)
- A free-standing operator!=(TypeA, TypeB)
- A free-standing operator==(TypeA, TypeB)
- A free-standing operator==(TypeB, TypeA)
- A member function TypeA::operator!=(TypeB)
- A member function TypeA::operator==(TypeB)
- A member function TypeB::operator==(TypeA)
x != y
the compiler might now try all of the following:x.operator!=(y) // calling member operator!= for x
operator!=(x, y) // calling a free-standing operator!= for x and y
!x.operator==(y) // calling member operator== for x
!operator==(x, y) // calling a free-standing operator== for x and y
!x.operator==(y) // calling member operator== generated by operator<=> for x
!y.operator==(x) // calling member operator== generated by operator<=> for y
The last form is tried to support an implicit type conversion for the first operand, which requires that the operand is a parameter.- A free-standing operator !=: operator!=(x, y)
- or a member operator !=: x.operator!=(y)
- A free-standing operator ==: !operator==(x, y)
- or a member operator ==: !x.operator==(y)
This also applies if the member operator== is generated due to a defaulted operator<=>.
42 != y // 42 implicitly converts to the type of y- A free-standing or member operator !=
- A free-standing or member operator == (note that the member operator == may be generated from a defaulted operator<=> member)
x <= yx.operator<=(y) // calling member operator<= for x
operator<=(x, y) // calling a free-standing operator<= for x and y
x.operator<=>(y) <= 0 // calling member operator<=> for x
operator<=>(x, y) <= 0 // calling a free-standing operator<=> for x and y
0 <= y.operator<=>(x) // calling member operator<=> for y
Operator <=>
- The return of <=> operator type should be marked as 'auto' and let compiler to deduce the type.
- Operator <=> takes precedence over all other comparison operators; except explicitly user defined.
- Should only call operator <=> directly when implementing operator<=>.
However, it can be very helpful to know the returned comparison category.
#include <compare>
// order of the members in the class matters.
class Value {
// defines the ordering and can be used by the relational operators <, <=, >, and >=.
auto operator<=> (const Value& rhs) const = default;
// implicitly generated
// defines equality and can be used by the equality operators == and !=.
auto operator== (const Value& rhs) const = default;
};
class Value {
private:
long id;
public:
constexpr Value(long i) noexcept
: id{i} {}
// for equality operators:
bool operator== (const Value& rhs) const {
return id == rhs.id; // defines equality (== and !=)
}
// for relational operators:
auto operator<=> (const Value& rhs) const {
return id <=> rhs.id; // defines ordering (<, <=, >, and >=)
}
};Compiler generated operator has following traits
- They are noexcept if comparing the members never throws
- They are constexpr if comparing the members is possible at compile time
- Thanks to rewriting, implicit type conversions for the first operand are also supported (This can also be tricky/buggy)
x <= y (x <=> y) <= 0;
// Or:
0 <= (y <=> x);- If the value of x<=>y is equal to 0, x and y are equal or equivalent.
- If the value of x<=>y is less than 0, x is less than y.
- If the value of x<=>y is greater than 0, x is greater than y.
- strong ordering,
- weak ordering,
- or partial ordering.
Comparison Category Types
strong ordering (total ordering):
std::strong_ordering operator<=> (MyType x, MyOtherType y)
{
if (xIsEqualToY) return std::strong_ordering::equal;
if (xIsLessThanY) return std::strong_ordering::less;
return std::strong_ordering::greater;
}
class MyType {
std::strong_ordering operator<=> (const MyType& rhs) const {
return value == rhs.value ? std::strong_ordering::equal :
value < rhs.value ? std::strong_ordering::less :
std::strong_ordering::greater;
}
type value;
};
// often
class MyType {
auto operator<=> (const MyType& rhs) const {
return value <=> rhs.value;
}
type value;
};if (!(x < y || y < x)) // might call operator<=> to check for equality
if (x <= y && y <= x) // might call operator<=> to check for equality Operator <=> return type mismatch due to multiple data members:
class Person {
std::string name;
double value;
std::partial_ordering operator<=> (const Person& rhs) const { // OK
auto cmp1 = name <=> rhs.name;
if (cmp1 != 0) return cmp1; // strong_ordering converted to return type
return value <=> rhs.value; // partial_ordering used as the return type
}
};
// better
class Person {
std::string name;
double value;
auto operator<=> (const Person& rhs) const
-> std::common_comparison_category_t<decltype(name <=> rhs.name),
decltype(value <=> rhs.value)> {
auto cmp1 = name <=> rhs.name;
if (cmp1 != 0) return cmp1; // used as or converted to common comparison type
return value <=> rhs.value; // used as or converted to common comparison type
}
};
// convert to same comparison category:
class Person {
std::string name;
double value;
std::strong_ordering operator<=> (const Person& rhs) const {
auto cmp1 = name <=> rhs.name;
if (cmp1 != 0) return cmp1; // return strong_ordering for std::string
// map floating-point comparison result to strong ordering:
// https://en.cppreference.com/w/cpp/utility/compare/strong_order
return std::strong_order(value, rhs.value);
}
};std::strong_order() yields a std::strong_ordering value according to the passed arguments as follows:
- Using std::strong_order(val1, val2) for the passed types if defined
- Otherwise, if the passed values are floating-point types, using the value of totalOrder() as specified in ISO/IEC/IEEE 60559 (for which, e.g., -0 is less than +0 and -NaN is less than any non-NAN value and +NaN)
- Using the new function object std::compare_three_way{}(val1, val2) if defined for the passed types std::compare_three_way use like std::less
class Person {
std::string name;
SomeType value;
std::strong_ordering operator<=> (const Person& rhs) const {
auto cmp1 = name <=> rhs.name;
if (cmp1 != 0) return cmp1; // return strong_ordering for std::string
// map weak/partial comparison result to strong ordering:
return std::compare_strong_order_fallback(value, rhs.value);
}
};Defaulted operator== and operator<=> contract:
Defaulted operator<=> implies Defaulted operator==
Thus the following is enough to support all six comparison operators for objects of the type Coord:
#include <compare>
struct Coord {
double x{};
double y{};
double z{};
auto operator<=>(const Coord&) const = default;
};- The defaulted operators require the support of the members and possible base classes
- Defaulted operators == require the support of == in the members and base classes.
- Defaulted operators <=> require the support of == and either an implemented operator < or a defaulted operator <=> in the members and base classes.
- The operator is noexcept if comparing the members guarantees not to throw.
- The operator is constexpr if comparing the members is possible at compile time.
- operators ==, <=, and >= yield true,
- operators !=, <, and > yield false,
- and <=> yields std::strong_ordering::equal.
template<typename T>
class Type {
public:
[[nodiscard]] virtual std::strong_ordering
operator<=>(const Type&) const requires(!std::same_as<T,bool>) = default;
};
// compiler generates equivalent to
template<typename T>
class Type {
public:
[[nodiscard]] virtual std::strong_ordering
operator<=> (const Type&) const requires(!std::same_as<T,bool>) = default;
[[nodiscard]] virtual bool
operator== (const Type&) const requires(!std::same_as<T,bool>) = default;
};Implementation of the Defaulted operator<=>:
- If operator<=> is defined for a member or base class, that operator is called.
- Otherwise, operator== and operator< are called to decide whether (from the point of view of the members or base classes)
struct B {
bool operator==(const B&) const;
bool operator<(const B&) const;
};
struct D : public B {
// return type can not be auto due to base type has the operator== and operator< defined
// because it cannot decide which ordering category the base class has.
// In that case, you need operator<=> in the base class too.
std::strong_ordering operator<=> (const D&) const = default;
// auto generated by compiler even
// operator<=> is declared as
// auto operator<=> (const D&) const = default;
// which then d1 > d2; does't work but d1 != d2; works.
bool operator== (const D&) const = default;
};
// Then:
D d1, d2;
d1 > d2; // calls B::operator== and possibly B::operator<
// If operator== yields true, we know that the result of > is false and that is it.
// Otherwise, operator< is called to find out whether the expression is true or false.Compare values of a generic type
template<typename T>
struct Value {
T val{};
...
auto operator<=> (const Value& v) const noexcept(noexcept(val<=>val)) {
return std::compare_three_way{}(val<=>v.val);
}
};
template<typename T>
struct Value {
T val{};
...
std::compare_three_way_result_t<T,T>
operator<=> (const Value& v) const noexcept(noexcept(val<=>val));
};
Appendix
namespace detail
{
template <unsigned int>
struct common_cmpcat_base { using type = void; };
template <>
struct common_cmpcat_base <0u> { using type = std::strong_ordering; };
template <>
struct common_cmpcat_base <2u> { using type = std::partial_ordering; };
template <>
struct common_cmpcat_base <4u> { using type = std::weak_ordering; };
template <>
struct common_cmpcat_base <6u> { using type = std::partial_ordering; };
} // namespace detail
template <class...Ts>
struct common_comparison_category :
detail::common_cmpcat_base <(0u | ... |
(std::is_same_v <Ts, std::strong_ordering> ? 0u :
std::is_same_v <Ts, std::weak_ordering> ? 4u :
std::is_same_v <Ts, std::partial_ordering> ? 2u : 1u)
)> {};