On Fri, 21 Nov 2025 at 10:02, Tomasz Kaminski <[email protected]> wrote:
>
>
>
> On Thu, Nov 20, 2025 at 6:01 PM Jonathan Wakely <[email protected]> wrote:
>>
>> This modifies the equality comparisons for std::expected so that they do
>> not use explicit conversions to bool, to match the constraints which are
>> specified in terms of "convertible to" which implies implicitly
>> convertible. As a result of those changes, we cannot use logical
>> expressions with && or || that involve comparisons of the contained
>> values, because x && (*y == *z) might do the wrong thing if *y == *z
>> does not return bool.
>>
>> Also add [[nodiscard]] attributes which were missing.
>>
>> The new lwg4366.cc testcase is a dg-do run test not dg-do compile,
>> because the original example won't compile with libstdc++ even after
>> these fixes. We constrain the std::expected comparison operators with
>> std::convertible_to<bool> and the pathological Bool type in the issue
>> doesn't satisfy that concept. So the new test replaces the deleted
>> explicit conversion oeprator in the issue with one that isn't deleted
>> but terminates if called. This ensures we don't call it, thus ensuring
>> that std::expected's comparisons do implicit conversions only.
>>
>> It's unclear to me whether using the convertible_to concept in
>> std::expected comparisons is conforming, or if we should switch to an
>> __implicitly_convertible_to<bool> concept which only uses
>> std::is_convertible_v<T, bool> and doesn't check for explicit
>> conversions. That can be addressed separately from this change.
>
> I think it should be conforming, and if it's not we should change the standard
> and not. So totally agree on a separate issue.

Yeah, maybe we should just open a library issue and then close it as
"NAD - using convertible_to is allowed, but not required. Only silly
programs care about the difference between the trait and the concept".


>>
>>
>> libstdc++-v3/ChangeLog:
>>
>>         * include/std/expected (operator==): Use implicit conversion to
>>         bool and do not use logical && and || with operands of unknown
>>         types. Add nodiscard attributes.
>>         * testsuite/20_util/expected/equality.cc: Test some missing
>>         cases which were not covered previously.
>>         * testsuite/20_util/expected/lwg4366.cc: New test.
>> ---
>>
>> Tested x86_64-linux.
>
> LGTM
>>
>>
>>  libstdc++-v3/include/std/expected             | 37 ++++++++++++++-----
>>  .../testsuite/20_util/expected/equality.cc    | 26 +++++++++++++
>>  .../testsuite/20_util/expected/lwg4366.cc     | 33 +++++++++++++++++
>>  3 files changed, 87 insertions(+), 9 deletions(-)
>>  create mode 100644 libstdc++-v3/testsuite/20_util/expected/lwg4366.cc
>>
>> diff --git a/libstdc++-v3/include/std/expected 
>> b/libstdc++-v3/include/std/expected
>> index 591fc72a4388..a03a7d3f722f 100644
>> --- a/libstdc++-v3/include/std/expected
>> +++ b/libstdc++-v3/include/std/expected
>> @@ -1163,15 +1163,17 @@ namespace __expected
>>             { __t == __u } -> convertible_to<bool>;
>>             { __e == __e2 } -> convertible_to<bool>;
>>           }
>> +       [[nodiscard]]
>>         friend constexpr bool
>>         operator==(const expected& __x, const expected<_Up, _Er2>& __y)
>>         noexcept(noexcept(bool(*__x == *__y))
>>                   && noexcept(bool(__x.error() == __y.error())))
>>         {
>> +         if (__x.has_value() != __y.has_value())
>> +           return false;
>>           if (__x.has_value())
>> -           return __y.has_value() && bool(*__x == *__y);
>> -         else
>> -           return !__y.has_value() && bool(__x.error() == __y.error());
>> +           return *__x == *__y;
>> +         return __x.error() == __y.error();
>>         }
>>
>>        template<typename _Up, same_as<_Tp> _Vp>
>> @@ -1179,19 +1181,29 @@ namespace __expected
>>           && requires (const _Tp& __t, const _Up& __u) {
>>             { __t == __u } -> convertible_to<bool>;
>>           }
>> +       [[nodiscard]]
>>         friend constexpr bool
>>         operator==(const expected<_Vp, _Er>& __x, const _Up& __v)
>>         noexcept(noexcept(bool(*__x == __v)))
>> -       { return __x.has_value() && bool(*__x == __v); }
>> +       {
>> +         if (__x.has_value())
>> +           return *__x == __v;
>> +         return false;
>> +       }
>>
>>        template<typename _Er2>
>>         requires requires (const _Er& __e, const _Er2& __e2) {
>>           { __e == __e2 } -> convertible_to<bool>;
>>         }
>> +       [[nodiscard]]
>>         friend constexpr bool
>>         operator==(const expected& __x, const unexpected<_Er2>& __e)
>>         noexcept(noexcept(bool(__x.error() == __e.error())))
>> -       { return !__x.has_value() && bool(__x.error() == __e.error()); }
>> +       {
>> +         if (!__x.has_value())
>> +           return __x.error() == __e.error();
>> +         return false;
>> +       }
>>
>>        friend constexpr void
>>        swap(expected& __x, expected& __y)
>> @@ -1841,24 +1853,31 @@ namespace __expected
>>           && requires (const _Er& __e, const _Er2& __e2) {
>>             { __e == __e2 } -> convertible_to<bool>;
>>           }
>> +       [[nodiscard]]
>>         friend constexpr bool
>>         operator==(const expected& __x, const expected<_Up, _Er2>& __y)
>
> We
>>
>>         noexcept(noexcept(bool(__x.error() == __y.error())))
>>
>>         {
>> +         if (__x.has_value() != __y.has_value())
>> +           return false;
>>           if (__x.has_value())
>> -           return __y.has_value();
>> -         else
>> -           return !__y.has_value() && bool(__x.error() == __y.error());
>> +           return true;
>> +         return __x.error() == __y.error();
>>         }
>>
>>        template<typename _Er2>
>>         requires requires (const _Er& __e, const _Er2& __e2) {
>>           { __e == __e2 } -> convertible_to<bool>;
>>         }
>> +       [[nodiscard]]
>>         friend constexpr bool
>>         operator==(const expected& __x, const unexpected<_Er2>& __e)
>>         noexcept(noexcept(bool(__x.error() == __e.error())))
>> -       { return !__x.has_value() && bool(__x.error() == __e.error()); }
>> +       {
>> +         if (!__x.has_value())
>> +           return __x.error() == __e.error();
>> +         return false;
>> +       }
>>
>>        friend constexpr void
>>        swap(expected& __x, expected& __y)
>> diff --git a/libstdc++-v3/testsuite/20_util/expected/equality.cc 
>> b/libstdc++-v3/testsuite/20_util/expected/equality.cc
>> index db19b1510a73..cc122f469082 100644
>> --- a/libstdc++-v3/testsuite/20_util/expected/equality.cc
>> +++ b/libstdc++-v3/testsuite/20_util/expected/equality.cc
>> @@ -28,19 +28,45 @@ test_eq()
>>    std::expected<int, int> e2;
>>    VERIFY(e2 == e2);
>>    VERIFY(e1 == e2);
>> +  VERIFY(e2 == e1);
>>    VERIFY(e1 != std::unexpected<int>(1));
>> +
>>    e1 = std::unexpected<int>(1);
>>    VERIFY(e1 == std::unexpected<int>(1));
>>    VERIFY(e1 != std::unexpected<int>(2));
>>    VERIFY(e1 != e2);
>> +  VERIFY(e2 != e1);
>> +  VERIFY(e1 != 1);
>> +
>> +  e2 = std::unexpected<int>(1);
>> +  VERIFY(e1 == e2);
>> +  VERIFY(e2 == e1);
>> +
>> +  e2 = std::unexpected<int>(2);
>> +  VERIFY(e1 != e2);
>> +  VERIFY(e2 != e1);
>>
>>    std::expected<void, int> e3;
>>    VERIFY(e3 == e3);
>>    VERIFY(e3 != std::unexpected<int>(1));
>> +  std::expected<const void, long> e4;
>> +  VERIFY(e3 == e4);
>> +  VERIFY(e4 == e3);
>> +
>>    e3 = std::unexpected<int>(1);
>>    VERIFY(e3 == e3);
>>    VERIFY(e3 == std::unexpected<int>(1));
>>    VERIFY(e3 != std::unexpected<int>(2));
>> +  VERIFY(e3 != e4);
>> +  VERIFY(e4 != e3);
>> +
>> +  e4 = e3;
>> +  VERIFY(e3 == e4);
>> +  VERIFY(e4 == e3);
>> +
>> +  e4 = std::unexpected<int>(4);
>> +  VERIFY(e3 != e4);
>> +  VERIFY(e4 != e3);
>>
>>    return true;
>>  }
>> diff --git a/libstdc++-v3/testsuite/20_util/expected/lwg4366.cc 
>> b/libstdc++-v3/testsuite/20_util/expected/lwg4366.cc
>> new file mode 100644
>> index 000000000000..35d53714f036
>> --- /dev/null
>> +++ b/libstdc++-v3/testsuite/20_util/expected/lwg4366.cc
>> @@ -0,0 +1,33 @@
>> +// { dg-do run { target c++23 } }
>> +
>> +// LWG 4366. Heterogeneous comparison of expected may be ill-formed
>> +
>> +#include <expected>
>> +#include <testsuite_hooks.h>
>> +
>> +struct Bool
>> +{
>> +  operator bool() const { return true; }
>> +  explicit operator bool() { throw; }
>> +};
>> +
>> +struct E1 {
>> +  friend Bool operator==(E1, E1) { return {}; }
>> +} e1;
>> +
>> +struct E2 {
>> +  friend Bool operator==(E1, E2) { return {}; }
>> +} e2;
>> +
>> +int main()
>> +{
>> +  std::expected<int, E1> u1(std::unexpect, e1);
>> +  VERIFY(u1 == u1);
>> +
>> +  std::unexpected<E2> u2(e2);
>> +  VERIFY(u1 == u2);
>> +
>> +  std::expected<void, E1> u3(std::unexpect, e1);
>> +  VERIFY(u3 == u3);
>> +  VERIFY(u3 == u2);
>> +}
>> --
>> 2.51.1
>>

Reply via email to