diff --git a/2944_comparisons_for_reference_wrapper/comparisons-for-reference_wrapper.md b/2944_comparisons_for_reference_wrapper/comparisons-for-reference_wrapper.md index 0c149157..e6fd4c03 100644 --- a/2944_comparisons_for_reference_wrapper/comparisons-for-reference_wrapper.md +++ b/2944_comparisons_for_reference_wrapper/comparisons-for-reference_wrapper.md @@ -11,7 +11,7 @@ toc: true # Revision History -Since [@P2944R1], added section on [ambiguity](#ambiguity) and updated wording accordingly. +Since [@P2944R1], added section on [ambiguity](#ambiguity-issues) and updated wording accordingly. Since [@P2944R0], fixed the wording @@ -103,7 +103,7 @@ if (std::ranges::any_of(v, equals(std::ref(target)))) { And this works! Just... only for some types, seemingly randomly. The goal of this proposal is for it to just always work. -## Ambiguity +## Ambiguity Issues In the original revision of the paper, the proposal was simply to add this equality operator: @@ -179,6 +179,69 @@ template class reference_wrapper { And that, now, passes [all the tests](https://godbolt.org/z/eTs71o49o). +## Non-boolean comparisons + +Another question that came up with in the LEWG telecon was how this proposal interacts with non-boolean comparison operators. For instance: + +::: bq +```cpp +void f(std::valarray v) { + // this is a valid expression today, whose type is not bool, but rather + // something convertible to std::valarray + v == v; +} +``` +::: + +Now, `std::valarray`'s comparison operators are specified as non-member function templates, so any comparison using `std::reference_wrapper>` doesn't work today. But let's make our own version of this type that's more friendly (or hostile, depending on your perspective) to this paper and consider: + +::: bq +```cpp +template +struct ValArray { + friend auto operator==(ValArray const&, ValArray const&) -> ValArray { + return {}; + } +}; + +void f(ValArray v) { + // this is valid and has type ValArray + v == v; + + // this is also valid today and has the same type + std::ref(v) == std::ref(v); +} +``` +::: + +Now, does anybody write such code? Who knows. If we [constrain](#constraints-vs-mandates) the comparisons of `std::reference_wrapper` (and also the other standard library types), then this code will continue to work fine anyway - since the comparisons would be constrained away by types like `ValArray` not satisfying `equality_comparable`. This paper would not be adding any new candidates to the candidate set, so no behavior changes. + +But, as always, there is an edge case. + +1. there is a type `T`, whose comparisons return a type like `int` +2. and those comparisons are written in such a way that comparison `T` to `std::reference_wrapper` works (see table above) +3. and users are relying on such comparisons to actually return `int` + +Then the comparisons to `std::reference_wrapper` will instead start returning `bool`. That is: + +::: bq +```cpp +struct ComparesAsInt { + friend auto operator==(ComparseAsInt, ComparesAsInt) -> int; +}; + +auto f(std::reference_wrapper a, std::reference_wrapper b) { + // today: compiles and returns int + // proposed: compiles and returns bool + return a == b; +} +``` +::: + +Here, the added comparison operators would be valid, and wouldn't constrain away, since `std::equality_comparable` is based on `$boolean-testable$` which only requires convertibility to `bool` (and some other nice behavior), which `int` does satisfy. And those added comparison operators would be better matches than the existing ones, so they would win. + +This would be the only case where any behavior would change. + # Proposal Add `==` and `<=>` to `std::reference_wrapper` so that `std::reference_wrapper` is always comparable when `T` is, regardless of how `T`'s comparisons are defined. diff --git a/2944_comparisons_for_reference_wrapper/p2944r2.html b/2944_comparisons_for_reference_wrapper/p2944r2.html index d5be9b6b..a0d271e1 100644 --- a/2944_comparisons_for_reference_wrapper/p2944r2.html +++ b/2944_comparisons_for_reference_wrapper/p2944r2.html @@ -566,7 +566,8 @@

Contents

  • 1 Revision History
  • 2 Introduction
  • 3 Proposal
      @@ -577,7 +578,7 @@

      Contents

    1 Revision History

    -

    Since [P2944R1], added section on ambiguity and updated wording accordingly.

    +

    Since [P2944R1], added section on ambiguity and updated wording accordingly.

    Since [P2944R0], fixed the wording

    2 Introduction

    Typically in libraries, wrapper types are comparable when their underlying types are comparable. tuple<T> is equality comparable when T is. optional<T> is equality comparable when T is. variant<T> is equality comparable when T is.

    @@ -679,7 +680,7 @@

    }

    And this works! Just… only for some types, seemingly randomly. The goal of this proposal is for it to just always work.

    -

    2.1 Ambiguity

    +

    2.1 Ambiguity Issues

    In the original revision of the paper, the proposal was simply to add this equality operator:

    template<class T> class reference_wrapper {
    @@ -731,62 +732,109 @@ 

    2.1}

    And that, now, passes all the tests.

    +

    2.2 Non-boolean comparisons

    +

    Another question that came up with in the LEWG telecon was how this proposal interacts with non-boolean comparison operators. For instance:

    +
    +
    void f(std::valarray<int> v) {
    +  // this is a valid expression today, whose type is not bool, but rather
    +  // something convertible to std::valarray<bool>
    +  v == v;
    +}
    +
    +

    Now, std::valarray<T>’s comparison operators are specified as non-member function templates, so any comparison using std::reference_wrapper<std::valarray<T>> doesn’t work today. But let’s make our own version of this type that’s more friendly (or hostile, depending on your perspective) to this paper and consider:

    +
    +
    template <typename T>
    +struct ValArray {
    +  friend auto operator==(ValArray const&, ValArray const&) -> ValArray<bool> {
    +    return {};
    +  }
    +};
    +
    +void f(ValArray<int> v) {
    +  // this is valid and has type ValArray<bool>
    +  v == v;
    +
    +  // this is also valid today and has the same type
    +  std::ref(v) == std::ref(v);
    +}
    +
    +

    Now, does anybody write such code? Who knows. If we constrain the comparisons of std::reference_wrapper<T> (and also the other standard library types), then this code will continue to work fine anyway - since the comparisons would be constrained away by types like ValArray<T> not satisfying equality_comparable. This paper would not be adding any new candidates to the candidate set, so no behavior changes.

    +

    But, as always, there is an edge case.

    +
      +
    1. there is a type T, whose comparisons return a type like int
    2. +
    3. and those comparisons are written in such a way that comparison T to std::reference_wrapper<T> works (see table above)
    4. +
    5. and users are relying on such comparisons to actually return int
    6. +
    +

    Then the comparisons to std::reference_wrapper<T> will instead start returning bool. That is:

    +
    +
    struct ComparesAsInt {
    +  friend auto operator==(ComparseAsInt, ComparesAsInt) -> int;
    +};
    +
    +auto f(std::reference_wrapper<ComparesAsInt> a, std::reference_wrapper<ComparesAsInt> b) {
    +  // today: compiles and returns int
    +  // proposed: compiles and returns bool
    +  return a == b;
    +}
    +
    +

    Here, the added comparison operators would be valid, and wouldn’t constrain away, since std::equality_comparable is based on boolean-testable which only requires convertibility to bool (and some other nice behavior), which int does satisfy. And those added comparison operators would be better matches than the existing ones, so they would win.

    +

    This would be the only case where any behavior would change.

    3 Proposal

    Add == and <=> to std::reference_wrapper<T> so that std::reference_wrapper<T> is always comparable when T is, regardless of how T’s comparisons are defined.

    Change 22.10.6.1 [refwrap.general]:

    -
      template<class T> class reference_wrapper {
    -  public:
    -    // types
    -    using type = T;
    -
    -    // [refwrap.const], constructors
    -    template<class U>
    -      constexpr reference_wrapper(U&&) noexcept(see below);
    -    constexpr reference_wrapper(const reference_wrapper& x) noexcept;
    -
    -    // [refwrap.assign], assignment
    -    constexpr reference_wrapper& operator=(const reference_wrapper& x) noexcept;
    -
    -    // [refwrap.access], access
    -    constexpr operator T& () const noexcept;
    -    constexpr T& get() const noexcept;
    -
    -    // [refwrap.invoke], invocation
    -    template<class... ArgTypes>
    -      constexpr invoke_result_t<T&, ArgTypes...> operator()(ArgTypes&&...) const
    -        noexcept(is_nothrow_invocable_v<T&, ArgTypes...>);
    -
    -+   // [refwrap.comparisons], comparisons
    -+   friend constexpr bool operator==(reference_wrapper, reference_wrapper);
    -+   friend constexpr bool operator==(reference_wrapper, const T&);
    -+   friend constexpr bool operator==(reference_wrapper, reference_wrapper<const T>);
    -
    -+   friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper, reference_wrapper);
    -+   friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper, const T&);
    -+   friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper, reference_wrapper<const T>);
    -  };
    +
      template<class T> class reference_wrapper {
    +  public:
    +    // types
    +    using type = T;
    +
    +    // [refwrap.const], constructors
    +    template<class U>
    +      constexpr reference_wrapper(U&&) noexcept(see below);
    +    constexpr reference_wrapper(const reference_wrapper& x) noexcept;
    +
    +    // [refwrap.assign], assignment
    +    constexpr reference_wrapper& operator=(const reference_wrapper& x) noexcept;
    +
    +    // [refwrap.access], access
    +    constexpr operator T& () const noexcept;
    +    constexpr T& get() const noexcept;
    +
    +    // [refwrap.invoke], invocation
    +    template<class... ArgTypes>
    +      constexpr invoke_result_t<T&, ArgTypes...> operator()(ArgTypes&&...) const
    +        noexcept(is_nothrow_invocable_v<T&, ArgTypes...>);
    +
    ++   // [refwrap.comparisons], comparisons
    ++   friend constexpr bool operator==(reference_wrapper, reference_wrapper);
    ++   friend constexpr bool operator==(reference_wrapper, const T&);
    ++   friend constexpr bool operator==(reference_wrapper, reference_wrapper<const T>);
    +
    ++   friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper, reference_wrapper);
    ++   friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper, const T&);
    ++   friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper, reference_wrapper<const T>);
    +  };

    Add a new clause, [refwrap.comparisons], after 22.10.6.5 [refwrap.invoke]:

    -
    friend constexpr bool operator==(reference_wrapper x, reference_wrapper y);
    +
    friend constexpr bool operator==(reference_wrapper x, reference_wrapper y);

    1 Mandates: The expression x.get() == y.get() is well-formed and its result is convertible to bool.

    2 Returns: x.get() == y.get().

    -
    friend constexpr bool operator==(reference_wrapper x, const T& y);
    +
    friend constexpr bool operator==(reference_wrapper x, const T& y);

    3 Mandates: The expression x.get() == y is well-formed and its result is convertible to bool.

    4 Returns: x.get() == y.

    -
    friend constexpr bool operator==(reference_wrapper x, reference_wrapper<const T> y);
    +
    friend constexpr bool operator==(reference_wrapper x, reference_wrapper<const T> y);

    5 Constraints: is_const_v<T> is false.

    6 Mandates: The expression x.get() == y.get() is well-formed and its result is convertible to bool.

    7 Returns: x.get() == y.get().

    -
    friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper x, reference_wrapper y);
    +
    friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper x, reference_wrapper y);

    8 Returns: synth-three-way(x.get(), y.get()).

    -
    friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper x, const T& y);
    +
    friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper x, const T& y);

    9 Returns: synth-three-way(x.get(), y).

    -
    friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper x, reference_wrapper<const T> y);
    +
    friend constexpr synth-three-way-result<T> operator<=>(reference_wrapper x, reference_wrapper<const T> y);

    10 Constraints: is_const_v<T> is false.

    11 Returns: synth-three-way(x.get(), y.get()).

    @@ -795,7 +843,7 @@

    std::reference_wrapper<T>, and there doesn’t seem like a good one to bump for this, so let’s add a new one to 17.3.2 [version.syn]

    -
    + #define __cpp_lib_reference_wrapper 20XXXXL // also in <functional>
    +
    + #define __cpp_lib_reference_wrapper 20XXXXL // also in <functional>

    3.2 Constraints vs Mandates