Showing posts with label cpp20_spaceship_operator. Show all posts
Showing posts with label cpp20_spaceship_operator. Show all posts

Oct 21, 2023

[C++20] Nicolai M. Josuttis's C++20 the complete guide reading minute - operator <=>

Comparisons and Operator <=>

Reference:

C++20 compiler rewrites
  1. Operator != with !(a==b)
  2. If above doesn't work, change the order of the operands. !(b==a)
And a, b can be different types.
If there's a free-standing operator:
  • 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)
Compiler can do the rewrite trick.


Thus To compile
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.

In general, the compiler tries to call:
  • A free-standing operator !=: operator!=(x, y)
  • or a member operator !=: x.operator!=(y)
Having both operators != defined is an ambiguity error.

  • A free-standing operator ==: !operator==(x, y)
  • or a member operator ==: !x.operator==(y)
Note that the member operator == may be generated from a defaulted operator<=> member.
Having both operators == defined is an ambiguity error.
This also applies if the member operator== is generated due to a defaulted operator<=>.

When an implicit type conversion for the first operand v is necessary, 
the compiler also tries to reorder the operands. Consider:
42 != y // 42 implicitly converts to the type of y
In that case, the compiler tries to call in that order:
  • 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)
Note that a rewritten expression never tries to call a member operator !=


Thus To compile
x <= y
new operator <=> and compare the result with 0. 
The operator behaves like a three-way comparison function
returning a negative value for less, 0 for equal, and a positive value for greater 
(the returned value is not a numeric value; it is only a value that supports the corresponding comparisons).
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) <= 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
The last form is tried to support an implicit type conversion for the first operand,
for which it has to become a parameter.


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)

C++20 compiler rewrites
If no operator<=:
x <= y
rewrites with:
  (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.
However, note that the return type of operator<=> is not an integral value.
Return type is a type that signals the comparison category, which could be 
  • strong ordering, 
  • weak ordering, 
  • or partial ordering.
These types support the comparison with 0 to deal with the result.

Note that operator<=> is for implementing types. Outside the implementation of an operator<=>,
programmers should never invoke <=> directly. Although you can, you should never write 
a<=>b < 0
instead of
a<b

Comparison Category Types

strong ordering (total ordering):

– std::strong_ordering::less
– std::strong_ordering::equal
(also available as std::strong_ordering::equivalent)
– std::strong_ordering::greater
Any value of a given type is less than or equal to or
greater than any other value of this type (including itself).

weak ordering:

– std::weak_ordering::less
– std::weak_ordering::equivalent
– std::weak_ordering::greater
Any value of a given type is less than or equivalent to or greater than any other
value of this type (including itself). However, equivalent values do not have to be equal
 (have the same value).

E.g. "hello" is equivalent to "HELLO"

– std::partial_ordering::less
– std::partial_ordering::equivalent
– std::partial_ordering::greater
– std::partial_ordering::unordered
Any value of a given type could be less than or equivalent to or greater than any
other value of this type (including itself). 
However, in addition, it may not be possible to specify a specific order between two values at all.

E.g. floating-point types, because they might have the special value
NaN (“not a number”). Any comparison with NaN yields false. Therefore, in this case a comparison
might yield that two values are unordered and the comparison operator might return one of four values.

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;
};

C++20 compiler rewrites
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 

For other types that have a weaker ordering and operators == and < defined, you can use the function
```
Performs three-way comparison on subexpressions t and u and produces a result of type std::strong_ordering, even if the operator <=> is unavailable.
```
accordingly:
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 second parameter as const lvalue reference (const &) in member function. 
Friend functions might alternatively take both parameters by value.

  • 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.

For empty classes, the defaulted operators compare all objects as equal: 
  • 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<=>:

Contract:
If operator<=> is defaulted and you have members or base classes and you call one of the relational
operators, then the following happens:
  • 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)
– The objects are equal/equivalent (operator== yields true)
– The objects are less or greater
– The objects are unordered (only when partial ordering is checked)
In this case, the return type of the defaulted operator<=> calling these operators cannot be auto.
For example, consider the following declarations:
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

Defines a total order for raw pointers.
For forward declare operator<=>() result type; use std::compare_three_way_result_t

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)
    )> {};

Nov 27, 2018

[C++][cppcon 2018] How to Write Well-Behaved Value Wrappers - Simon Brand


Value Wrappers

Types with value-semantics which can store objects of any type.
Do what 'int' does.
i.e
std::pair
std::optional
std::variant


Traits that value wrappers should have

  • Performant
  • Unsurprising


Hey, beware of code's 'hot paths'!


Let's start:

Comparison Operators

Weak ordering, i.e equivalence rather than equality.
Strong ordering, i.e equality rather than equivalence.

Relation strength
(a <=> b) < 0  //true if a < b
(a <=> b) > 0  //true if a > b
(a <=> b) == 0 //true if a is equal/equivalent to b


spaceship operator returns types as follows:
Five types are provided, and stronger relations can implicitly convert to weaker ones:

(figure credits: https://blog.tartanllama.xyz/spaceship-operator/ )


Why those types?
1. Indicates to the user what kind of relation is modeled by the comparisons.
2. Algorithms takes advantage of being optimized.
i.e If the operands are compared equal with strongly-ordered type, it indicates
that any function taking either operand should give out the same result, thus
call the function once with a operand would be enough.
3. These types can be used to define language features, i.e
Class Types in Non-Type Template Parameters
( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0732r0.pdf ),
i.e reduce template instantiation if the non-value type argument is strongly-ordered equal.

spaceship operator doesn't need to be free function like binary operators:
struct foo {
  int i;

  std::strong_ordering operator<=> (foo const& rhs) {
    return i <=> rhs.i;
  }
};

Use std::compare_three_way to fall back to two-way comparisons if there's
no three-way comparisons available.

Write a three-way comparisons operator for std::pair:
template<class T, class U>
struct pair {
  T t;
  U u;

  auto operator<=> (pair const& rhs) const
    -> std::common_comparison_category_t<
         decltype(std::compare_three_way{}(t, rhs.t)),
         decltype(std::compare_three_way{}(u, rhs.u)> {
    if (auto cmp = std::compare_three_way{}(t, rhs.t); cmp != 0) return cmp;
    return std::compare_three_way{}(u, rhs.u);
  }

std::common_comparison_category_t determines the weakest relation
in it's arguments.
i.e
std::common_comparison_category_t<std::strong_ordering, std::partial_ordering>
is std::partial_ordering

C++20 supports automatic generation of comparison operators.
auto operator<=>(x const&) = default;

Reference:
Implementing the spaceship operator for optional
Spaceship Operator
https://en.cppreference.com/w/cpp/header/compare


'noexcept' Propagation

Propagate the noexcept-ness of 'move' and 'swap' operations.
Why? Cause std containers honor noexcept 'move' constructor.


'explicit' or not 'explicit'

// OK but with code duplication.
template<typename U, std::enable_if_t<
    std::is_constructible<T, U>::value &&
    std::is_convertible<U, T>::value>* = nullptr>
wrapper(U&& u) : t(std::forward<U>(u)) {}


template<typename U, std::enable_if_t<
    std::is_constructible<T, U>::value &&
    !std::is_convertible<U, T>::value>* = nullptr>
explicit wrapper(U&& u) : t(std::forward<U>(u)) {}
to:
template<typename U, std::enable_if_t<
    std::is_constructible<T, U>::value>* = nullptr>
explicit(std::is_convertible<U, T>::value>)   // https://en.cppreference.com/w/cpp/language/explicit C++20
wrapper(U&& u) : t(std::forward<U>(u)) {}


Conditionally deleting special members

once touches the Rule of 5, it's zero-sum situation.
Use base class for rule of 5's delete/default and use private inherits.
Use concept.
optional(optional const& rhs) {
    if (rhs.enaged) {
        new (std::addressof(t)) T (rhs.t);
        engaged = true;
    }
}


Triviality propagation

'An object with either a non-trivial copy constructor or a non-trivial destructor
cannot be passed by value because such objects must have well defined addresses.'
This effects RVO.
We cannot use SFINAE on destructor/copy constructor.
However, we can again use base type and with private inherits.
Use concept to simplify the code.


Ref-qualified accessor functions:
template<typename Self>
decltype(auto) operator*(this Self&& self) {
    return std::forward<Self>(self).m_value;
}


SFINAE-unfriendly callables

Expression SFINAE: https://stackoverflow.com/a/12654277
consteval in C++20
SFINAE only works at:
quoted:
--
Only the failures in the types and expressions in the immediate context of the function type or its template parameter types or its explicit specifier (since C++20) are SFINAE errors.

If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors.

A lambda expression is not considered part of the immediate context. (since C++20)
--
Be ware of SFINAE's hard errors.
#include <iostream>
#include <memory>

using namespace std;

struct foo {
    void do_thing()
    {
        cout << "do thing" << endl;
    }
};

template <typename T>
struct wrapper {
    T t;
    template <typename F>
    auto pass_to(F f) -> decltype(f(t)) // expression SFINAE check
    {
        f(t);
        cout << "no const" << endl;
    }

    template <typename F>
    auto pass_to(F f) const -> decltype(f(t)) // expression SFINAE check
    {
        f(t);
        cout << "const" << endl;
    }

    template <typename F>
    auto pass_to_no_decltype(F f)
    {
        f(t);
        cout << "no const" << endl;
    }

    template <typename F>
    auto pass_to_no_decltype(F f) const
    {
        f(t);
        cout << "const" << endl;
    }
};


int main()
{
    const wrapper<foo> f{foo{}};
    // hard error; error: 'this' argument to member function 'do_thing' has type 'const foo', but function is not marked const
    // f.pass_to([](auto &&x) { x.do_thing(); }); 

	// hard error;  error: 'this' argument to member function 'do_thing' has type 'const foo', but function is not marked const
    // f.pass_to([](auto &x) { x.do_thing(); });  

    f.pass_to([](auto x) { x.do_thing(); });

    // Using const this to differentiate.
    f.pass_to_no_decltype([](auto &&x) { x.do_thing(); });
    f.pass_to_no_decltype([](auto &x) { x.do_thing(); });
    f.pass_to_no_decltype([](auto x) { x.do_thing(); });
}

Simon uses 'this Self&& self' signature,
i.e take advantage of re-appearing the
'this' pointer(r-value), like in Python, to solve the problem,
due to 'this' is c.v qualified.
template<typename T>
struct wrapper {
    T t;
    template<typename Self, typename F>
    auto pass_to (this Self&& self, F f) -> decltype(f(self.t)) {
        f(t);
    }
};