Quantcast
Channel: What are the basic rules and idioms for operator overloading? - Stack Overflow
Viewing all articles
Browse latest Browse all 11

Answer by Jan Schultke for What are the basic rules and idioms for operator overloading?

$
0
0

Comparison Operators, including Three-Way Comparison(C++20)

There are equality comparisons== and !=,and relational comparisons<, >, <=, >=.C++20 has also introduced the three-way comparison operator <=>.

OperatorMeaning and Notes (Old)Meaning and Notes (C++20)
x == ytrue if x and y are equal

satisfies EqualityComparable
(used by std::unordered_map)
(x <=> y) == 0
(usually implemented directly, not
delegated to three-way unless = default)

satisfies std::equality_comparable
x != y!(x == y)!(x == y)
x < ytrue if x is lower than y

satisfies LessThanComparable
(used by std::set, std::sort, etc.
but requires strict weak ordering)
(x <=> y) < 0

may satisfy std::strict_weak_ordering
when wrapped in a functor
(e.g. std::ranges::less)
x > yy < x(x <=> y) > 0
x <= y!(x < y) for strong orderings,
x == y || x < y otherwise
(x <=> y) <= 0
x >= yy <= x(x <=> y) >= 0
x <=> yN/Athree-way comparison
aka. "spaceship operator"

satisfies std::three_way_comparable

Guidelines

  1. Comparison operators shouldn't be member functions.1)
  2. If you define ==, define != too (unless it is rewritten in C++20).
  3. If you define <, define >, <=, and >= too.
  4. (C++20) Prefer defining <=> over defining each relational operator.
  5. (C++20) Prefer defaulting operators over implementing manually.
  6. Equality and relational comparisons should match, meaning that
    x == y should be equivalent to !(x < y) && !(y < x)2)
  7. Don't define == in terms of <, even when you could 3)

1)Otherwise, implicit conversions would be asymmetrical, and == is expected to apply the same kinds of implicit conversions to both sides.
2)This equivalence does not apply to float, but does apply to int and other strongly ordered types.
3)This is motivated by readability, correctness, and performance.

Implementation and Common Idioms Prior to C++20

Disclaimer
If you're using C++20, the implementations in this section have been obsoleted.
Skip ahead to the C++20 parts unless you're interested in a historical perspective.

All operators are typically implemented as non-member functions, possibly as hidden friends (friends where the function is defined inside the class).All following code examples use hidden friends because this becomes necessary if you need to compare private members anyway.

struct S {    int x, y, z;    // (In)equality comparison:    // implementing a member-wise equality    friend bool operator==(const S& l, const S& r) {        return l.x == r.x && l.y == r.y && l.z == r.z;    }    friend bool operator!=(const S& l, const S& r) { return !(l == r); }    // Relational comparisons:    // implementing a lexicographical comparison which induces a    // strict weak ordering.    friend bool operator<(const S& l, const S& r) {        if (l.x < r.x) return true;   // notice how all sub-comparisons        if (r.x < l.x) return false;  // are implemented in terms of <        if (l.y < r.y) return true;        if (r.y < l.y) return false; // also see below for a possibly simpler        return l.z < r.z;            // implementation    }    friend bool operator>(const S& l, const S& r) { return r < l; }    friend bool operator<=(const S& l, const S& r) { return !(r < l); }    friend bool operator>=(const S& l, const S& r) { return !(l < r); }};

Note: in C++11, all of these can typically be noexcept and constexpr.

Implementing all relational comparisons in terms of < is not valid if we have a partially ordered member (e.g. float).In that case, <= and >= must be written differently.

friend bool operator<=(const S& l, const S& r) { return l == r || l < r; }friend bool operator>=(const S& l, const S& r) { return r <= l; }

Further Notes on operator<

The implementation of operator< is not so simple because a proper lexicographical comparison cannot simply compare each member once.{1, 2} < {3, 0} should be true, even though 2 < 0 is false.

A lexicographical comparison is a simple way of implementing a strict weak ordering, which is needed for containers like std::set and algorithms like std::sort. In short, a strict weak ordering should behave like the < operator for integers, except that some integers are allowed to be equivalent (e.g. for all even integers, x < y is false).

If x != y is equivalent to x < y || y < x, a simpler approach is possible:

friend bool operator<(const S& l, const S& r) {    if (l.x != r.x) return l.x < r.x;    if (l.y != r.y) return l.y < r.y;    return l.z < r.z;}

Common Idioms

For multiple members, you can use std::tie to implement comparison lexicographically:

#include <tuple>struct S {    int x, y, z;    friend bool operator<(const S& l, const S& r) {        return std::tie(l.x, l.y, l.z) < std::tie(r.x, r.y, r.z);    }};

Use std::lexicographical_compare for array members.

Some people use macros or the curiously recurring template pattern (CRTP) to save the boilerplate of delegating !=, >, >=, and <=, or to imitate C++20's three-way comparison.

It is also possible to use std::rel_ops (deprecated in C++20) to delegate !=, >, <=, and >= to < and == for all types in some scope.


Default Comparisons (C++20)

A substantial amount of comparison operators simply compare each member of a class.If so, the implementation is pure boilerplate and we can let the compiler do it all:

struct S {    int x, y, z;    // ==, !=, <, >, <=, >= are all defined.    // constexpr and noexcept are inferred automatically.    friend auto operator<=>(const S&, const S&) = default;};

Note: defaulted comparison operators need to be friends of the class, and the easiest way to accomplish that is by defining them as defaulted inside the class. This makes them "hidden friends".

Alternatively, we can default individual comparison operators.This is useful if we want to define equality comparison, or only relational comparison:

friend bool operator==(const S&, const S&) = default; // inside S

See the cppreference article on default comparison.

Expression Rewriting (C++20)

In C++20, if a comparison isn't directly implemented, the compiler will also try to use rewrite candidates.Thanks to this, even if <=> isn't defaulted (which would implement all operators), we only have to implement == and <=>, and all other comparisons are rewritten in terms of these two.

OperatorPotential Rewrites
x == yy == x
x != y!(x == y) or !(y == x) if equality comparison returns bool
x < y(x <=> y) < 0 or 0 < (y <=> x) if comparison result is comparable to zero
x > y(x <=> y) > 0 or 0 > (y <=> x) if ...
x <= y(x <=> y) <= 0 or 0 <= (y <=> x) if ...
x >= y(x <=> y) >= 0 or 0 >= (y <=> x) if ...
struct S {    int x, y, z;    // ==, !=    friend constexpr bool operator==(const S& l, const S& r) noexcept { /* ... */ }    // <=>, <, >, <=, >=    friend constexpr auto operator<=>(const S& l, const S& r) noexcept { /* ... */ }};

Note: constexpr and noexcept are optional, but can almost always be applied to comparison operators.

Three-Way Comparison Operator (C++20)

Note: it is colloquially called "spaceship operator". See also .

The basic idea behind x <=> y is that the result tells us whether x is lower than, greater than, equivalent to, or unordered with y.This is similar to functions like strcmp in C.

// old C styleint compare(int x, int y) {    if (x < y) return -1;    if (x > y) return  1;    return             0; // or simply return (x > y) - (x < y);}// C++20 style: this is what <=> does for int.auto compare_cxx20(int x, int y) {    if (x < y) return std::strong_ordering::less;    if (x > y) return std::strong_ordering::greater;    return            std::strong_ordering::equal;}// This is what <=> does for float.auto compare_cxx20(float x, float y) {    if (x < y)  return std::partial_ordering::less;    if (x > y)  return std::partial_ordering::greater;    if (x == y) return std::partial_ordering::equivalent;    return             std::partial_ordering::unordered; // NaN}

Comparison Categories

The result of this operator is neither bool nor int, but a value of comparison category.

Comparison CategoryExamplePossible Values
std::strong_orderingintless, equal = equivalent, greater
std::weak_orderinguser-defined1)less, equivalent, greater
std::partial_orderingfloatless, equivalent, greater, unordered

std::strong_orderings can be converted to std::weak_ordering, which can be converted to std::partial_ordering.Values of these categories are comparable to (e.g. (x <=> y) == 0) and this has similar meaning to the compare function above.However, std::partial_ordering::unordered returns false for all comparisons.


1)There are no fundamental types for which x <=> y results in std::weak_ordering. Strong and weak orderings are interchangeable in practice; see Practical meaning of std::strong_ordering and std::weak_ordering.

Manual Implementation of Three-Way Comparison

Three-way comparison is often defaulted, but could be implemented manually like:

#include <compare> // necessary, even if we don't use std::is_eqstruct S {    int x, y, z;    // This implementation is the same as what the compiler would do    // if we defaulted <=> with = default;    friend constexpr auto operator<=>(const S& l, const S& r) noexcept {        // C++17 if statement with declaration makes this more readable.        // !std::is_eq(c) is not the same as std::is_neq(c); it is also true        // for std::partial_order::unordered.        if (auto c = l.x <=> r.x; !std::is_eq(c)) /* 1) */ return c;        if (auto c = l.y <=> r.y; !std::is_eq(c)) return c;        return l.y <=> r.y;    }    // == is not automatically defined in terms of <=>.    friend constexpr bool operator==(const S&, const S&) = default;};

If all members of S weren't the same type, then we could either specify the category explicitly (in the return type), or we could obtain it with std::common_comparison_category:

std::common_comparison_category_t<decltype(l.x <=> l.x), /* ... */>

1)Helper functions like std::is_neq compare the result of <=> to zero.They express intent more clearly, but you don't have to use them.

Common Idioms

Alternatively, we can let std::tie figure out the details:

#include <tuple>struct S {    int x, y, z;    friend constexpr auto operator<=>(const S& l, const S& r) noexcept {        return std::tie(l.x, l.y, l.z) <=> std::tie(r.x, r.y, r.z);    }};

Use std::lexicographical_compare_three_way for array members.


Viewing all articles
Browse latest Browse all 11

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>