Skip to content

[libc++] Implement P3379R0 Constrain std::expected equality operators #135759

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libcxx/docs/Status/Cxx2cPapers.csv
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"","","","","",""
"`P3136R1 <https://wg21.link/P3136R1>`__","Retiring niebloids","2024-11 (Wrocław)","","",""
"`P3138R5 <https://wg21.link/P3138R5>`__","``views::cache_latest``","2024-11 (Wrocław)","","",""
"`P3379R0 <https://wg21.link/P3379R0>`__","Constrain ``std::expected`` equality operators","2024-11 (Wrocław)","","",""
"`P3379R0 <https://wg21.link/P3379R0>`__","Constrain ``std::expected`` equality operators","2024-11 (Wrocław)","|Complete|","21",""
"`P2862R1 <https://wg21.link/P2862R1>`__","``text_encoding::name()`` should never return null values","2024-11 (Wrocław)","","",""
"`P2897R7 <https://wg21.link/P2897R7>`__","``aligned_accessor``: An ``mdspan`` accessor expressing pointer over-alignment","2024-11 (Wrocław)","|Complete|","21",""
"`P3355R1 <https://wg21.link/P3355R1>`__","Fix ``submdspan`` for C++26","2024-11 (Wrocław)","","",""
Expand Down
42 changes: 37 additions & 5 deletions libcxx/include/__expected/expected.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <__type_traits/is_assignable.h>
#include <__type_traits/is_constructible.h>
#include <__type_traits/is_convertible.h>
#include <__type_traits/is_core_convertible.h>
#include <__type_traits/is_function.h>
#include <__type_traits/is_nothrow_assignable.h>
#include <__type_traits/is_nothrow_constructible.h>
Expand Down Expand Up @@ -1139,8 +1140,15 @@ class expected : private __expected_base<_Tp, _Err> {

// [expected.object.eq], equality operators
template <class _T2, class _E2>
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const expected<_T2, _E2>& __y)
requires(!is_void_v<_T2>)
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const expected<_T2, _E2>& __y) {
# if _LIBCPP_STD_VER >= 26
&& requires {
{ *__x == *__y } -> __core_convertible_to<bool>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason why you don't use convertible_to<bool> ?

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've told the reason in #117664 (comment).

convertible_to requires both implicit and explicit convertibility, where the standard wording only requires the latter. Moreover, due to explicit object member functions and CWG2813, we can invent a class whose prvalue is implicitly convertible to bool but xvalue isn't.

Example (Godbolt link):

#include <type_traits>
#include <concepts>

struct X {
    X() = default;
    X(const X&) = delete;
    X& operator=(const X&) = delete;

    template<class = void>
    constexpr operator bool(this X) { return true; }
    explicit operator bool(this X) = delete;
};

static_assert(!std::is_convertible_v<X, bool>);
static_assert(!std::convertible_to<X, bool>);

constexpr bool b = X{};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say it is very contrived and I would hardly believe the standard is designed to be in this form. IMO, implicit conversion should be a superset of explicit conversion. Implicitly conversion should imply explicit conversion.

Having something is implicitly convertible to, but not explicitly convertible to , is , just , a foot gun.

This similar to non-const vs const. The example above is in the same spirit of having

void f(const Foo&);
void f(Foo&) = delete;

I would say, why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's intended to allow such a return type. However, the core language rules "accidentally" permits such a weird return type when there's no additional constraints/static_assert's.

IMO, P3379R0 and P2944R3 (for optional, variant, and expected) should only add constraints that reflects the "mandates" in the return statements. It seems that the original intent of operators was that natural mandates in the return statements should work for these operators, and thus no additional static_assert would be necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, even when there's no issue with explicit conversion, the prvalue VS xvalue problem still blocks use of convertible_to. Godbolt link.

#include <type_traits>
#include <concepts>

struct Y {
    Y() = default;
    Y(const Y&) = delete;
    Y& operator=(const Y&) = delete;

    constexpr operator bool(this Y) { return true; }
};

static_assert(!std::is_convertible_v<Y, bool>);
static_assert(!std::convertible_to<Y, bool>);

constexpr bool b1 = Y{};
constexpr bool b2(Y{});

{ __x.error() == __y.error() } -> __core_convertible_to<bool>;
}
# endif
{
if (__x.__has_val() != __y.__has_val()) {
return false;
} else {
Expand All @@ -1153,12 +1161,24 @@ class expected : private __expected_base<_Tp, _Err> {
}

template <class _T2>
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const _T2& __v) {
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const _T2& __v)
# if _LIBCPP_STD_VER >= 26
requires(!__is_std_expected<_T2>::value) && requires {
{ *__x == __v } -> __core_convertible_to<bool>;
}
# endif
{
return __x.__has_val() && static_cast<bool>(__x.__val() == __v);
}

template <class _E2>
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const unexpected<_E2>& __e) {
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const unexpected<_E2>& __e)
# if _LIBCPP_STD_VER >= 26
requires requires {
{ __x.error() == __e.error() } -> __core_convertible_to<bool>;
}
# endif
{
return !__x.__has_val() && static_cast<bool>(__x.__unex() == __e.error());
}
};
Expand Down Expand Up @@ -1851,7 +1871,13 @@ class expected<_Tp, _Err> : private __expected_void_base<_Err> {
// [expected.void.eq], equality operators
template <class _T2, class _E2>
requires is_void_v<_T2>
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const expected<_T2, _E2>& __y) {
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const expected<_T2, _E2>& __y)
# if _LIBCPP_STD_VER >= 26
requires requires {
{ __x.error() == __y.error() } -> __core_convertible_to<bool>;
}
# endif
{
if (__x.__has_val() != __y.__has_val()) {
return false;
} else {
Expand All @@ -1860,7 +1886,13 @@ class expected<_Tp, _Err> : private __expected_void_base<_Err> {
}

template <class _E2>
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const unexpected<_E2>& __y) {
_LIBCPP_HIDE_FROM_ABI friend constexpr bool operator==(const expected& __x, const unexpected<_E2>& __y)
# if _LIBCPP_STD_VER >= 26
requires requires {
{ __x.error() == __y.error() } -> __core_convertible_to<bool>;
}
# endif
{
return !__x.__has_val() && static_cast<bool>(__x.__unex() == __y.error());
}
};
Expand Down
7 changes: 7 additions & 0 deletions libcxx/include/__type_traits/is_core_convertible.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ template <class _Tp, class _Up>
struct __is_core_convertible<_Tp, _Up, decltype(static_cast<void (*)(_Up)>(0)(static_cast<_Tp (*)()>(0)()))>
: true_type {};

#if _LIBCPP_STD_VER >= 20

template <class _Tp, class _Up>
concept __core_convertible_to = __is_core_convertible<_Tp, _Up>::value;

#endif // _LIBCPP_STD_VER >= 20

_LIBCPP_END_NAMESPACE_STD

#endif // _LIBCPP___TYPE_TRAITS_IS_CORE_CONVERTIBLE_H
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@
#include <utility>

#include "test_macros.h"
#include "../../types.h"

struct Data {
int i;
constexpr Data(int ii) : i(ii) {}

friend constexpr bool operator==(const Data& data, int ii) { return data.i == ii; }
};
#if TEST_STD_VER >= 26
// https://wg21.link/P3379R0
static_assert(CanCompare<std::expected<int, int>, int>);
static_assert(CanCompare<std::expected<int, int>, EqualityComparable>);
static_assert(!CanCompare<std::expected<int, int>, NonComparable>);
#endif

constexpr bool test() {
// x.has_value()
{
const std::expected<Data, int> e1(std::in_place, 5);
const std::expected<EqualityComparable, int> e1(std::in_place, 5);
int i2 = 10;
int i3 = 5;
assert(e1 != i2);
Expand All @@ -37,7 +38,7 @@ constexpr bool test() {

// !x.has_value()
{
const std::expected<Data, int> e1(std::unexpect, 5);
const std::expected<EqualityComparable, int> e1(std::unexpect, 5);
int i2 = 10;
int i3 = 5;
assert(e1 != i2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@
#include <utility>

#include "test_macros.h"
#include "../../types.h"

// Test constraint
template <class T1, class T2>
concept CanCompare = requires(T1 t1, T2 t2) { t1 == t2; };

struct Foo{};
static_assert(!CanCompare<Foo, Foo>);
static_assert(!CanCompare<NonComparable, NonComparable>);

static_assert(CanCompare<std::expected<int, int>, std::expected<int, int>>);
static_assert(CanCompare<std::expected<int, int>, std::expected<short, short>>);

// Note this is true because other overloads are unconstrained
static_assert(CanCompare<std::expected<int, int>, std::expected<void, int>>);

#if TEST_STD_VER >= 26
// https://wg21.link/P3379R0
static_assert(!CanCompare<std::expected<int, int>, std::expected<void, int>>);
static_assert(CanCompare<std::expected<int, int>, std::expected<int, int>>);
static_assert(!CanCompare<std::expected<NonComparable, int>, std::expected<NonComparable, int>>);
static_assert(!CanCompare<std::expected<int, NonComparable>, std::expected<int, NonComparable>>);
static_assert(!CanCompare<std::expected<NonComparable, int>, std::expected<int, NonComparable>>);
static_assert(!CanCompare<std::expected<int, NonComparable>, std::expected<NonComparable, int>>);
#else
// Note this is true because other overloads in expected<non-void> are unconstrained
static_assert(CanCompare<std::expected<void, int>, std::expected<int, int>>);
#endif
constexpr bool test() {
// x.has_value() && y.has_value()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@
#include <utility>

#include "test_macros.h"
#include "../../types.h"

struct Data {
int i;
constexpr Data(int ii) : i(ii) {}

friend constexpr bool operator==(const Data& data, int ii) { return data.i == ii; }
};
#if TEST_STD_VER >= 26
// https://wg21.link/P3379R0
static_assert(CanCompare<std::expected<EqualityComparable, EqualityComparable>, std::unexpected<int>>);
static_assert(CanCompare<std::expected<EqualityComparable, int>, std::unexpected<EqualityComparable>>);
static_assert(!CanCompare<std::expected<EqualityComparable, NonComparable>, std::unexpected<int>>);
#endif

constexpr bool test() {
// x.has_value()
{
const std::expected<Data, Data> e1(std::in_place, 5);
const std::expected<EqualityComparable, EqualityComparable> e1(std::in_place, 5);
std::unexpected<int> un2(10);
std::unexpected<int> un3(5);
assert(e1 != un2);
Expand All @@ -37,7 +38,7 @@ constexpr bool test() {

// !x.has_value()
{
const std::expected<Data, Data> e1(std::unexpect, 5);
const std::expected<EqualityComparable, EqualityComparable> e1(std::unexpect, 5);
std::unexpected<int> un2(10);
std::unexpected<int> un3(5);
assert(e1 != un2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,26 @@
#include <utility>

#include "test_macros.h"

// Test constraint
template <class T1, class T2>
concept CanCompare = requires(T1 t1, T2 t2) { t1 == t2; };
#include "../../types.h"

struct Foo{};
static_assert(!CanCompare<Foo, Foo>);

static_assert(CanCompare<std::expected<void, int>, std::expected<void, int>>);
static_assert(CanCompare<std::expected<void, int>, std::expected<void, short>>);

#if TEST_STD_VER >= 26
// https://wg21.link/P3379R0
static_assert(!CanCompare<std::expected<void, int>, std::expected<int, int>>);
static_assert(CanCompare<std::expected<void, int>, std::expected<void, int>>);
static_assert(CanCompare<std::expected<void, int>, std::expected<void, int>>);
static_assert(!CanCompare<std::expected<void, NonComparable>, std::expected<void, NonComparable>>);
static_assert(!CanCompare<std::expected<void, int>, std::expected<void, NonComparable>>);
static_assert(!CanCompare<std::expected<void, NonComparable>, std::expected<void, int>>);
#else
// Note this is true because other overloads in expected<non-void> are unconstrained
static_assert(CanCompare<std::expected<void, int>, std::expected<int, int>>);
#endif

constexpr bool test() {
// x.has_value() && y.has_value()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@
#include <utility>

#include "test_macros.h"
#include "../../types.h"

struct Data {
int i;
constexpr Data(int ii) : i(ii) {}

friend constexpr bool operator==(const Data& data, int ii) { return data.i == ii; }
};
#if TEST_STD_VER >= 26
// https://wg21.link/P3379R0
static_assert(CanCompare<std::expected<void, EqualityComparable>, std::unexpected<int>>);
static_assert(CanCompare<std::expected<void, int>, std::unexpected<EqualityComparable>>);
static_assert(!CanCompare<std::expected<void, NonComparable>, std::unexpected<int>>);
#endif

constexpr bool test() {
// x.has_value()
{
const std::expected<void, Data> e1;
const std::expected<void, EqualityComparable> e1;
std::unexpected<int> un2(10);
std::unexpected<int> un3(5);
assert(e1 != un2);
Expand All @@ -37,7 +38,7 @@ constexpr bool test() {

// !x.has_value()
{
const std::expected<void, Data> e1(std::unexpect, 5);
const std::expected<void, EqualityComparable> e1(std::unexpect, 5);
std::unexpected<int> un2(10);
std::unexpected<int> un3(5);
assert(e1 != un2);
Expand Down
13 changes: 13 additions & 0 deletions libcxx/test/std/utilities/expected/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,17 @@ struct CheckForInvalidWrites : public CheckForInvalidWritesBase<WithPaddedExpect
}
};

struct NonComparable {};

struct EqualityComparable {
int i;
constexpr EqualityComparable(int ii) : i(ii) {}

friend constexpr bool operator==(const EqualityComparable& data, int ii) { return data.i == ii; }
};

// Test constraint
template <class T1, class T2>
concept CanCompare = requires(T1 t1, T2 t2) { t1 == t2; };

#endif // TEST_STD_UTILITIES_EXPECTED_TYPES_H
Loading