https://gcc.gnu.org/bugzilla/show_bug.cgi?id=120388
Bug ID: 120388 Summary: constraint on expected's comparison operator causes infinite recursion, overload resolution fails Product: gcc Version: 15.1.1 Status: UNCONFIRMED Severity: normal Priority: P3 Component: libstdc++ Assignee: unassigned at gcc dot gnu.org Reporter: justend29 at gmail dot com Target Milestone: --- ISSUE ===== The requires clause introduced in GCC15 on std::expected<T,E>::operator== prevents any type from being compared if it has std::expected as a template argument, as evaluation of the constraint depends on itself. The specific function causing the error is here (https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/include/std/expected;h=5dc1dfbe5b8a954826d2779a9cbc51c953b5e5f0;hb=1b306039ac49f8ad91ca71d3de3150a3c9fa792a#l1172), where its definition is as such: 1172 template<typename _Up> 1173 requires (!__expected::__is_expected<_Up>) 1174 && requires (const _Tp& __t, const _Up& __u) { 1175 { __t == __u } -> convertible_to<bool>; 1176 } 1177 friend constexpr bool 1178 operator==(const expected& __x, const _Up& __v) 1179 noexcept(noexcept(bool(*__x == __v))) 1180 { return __x.has_value() && bool(*__x == __v); } Minimum Reproducible Example ============================ Included below is a minimum reproducible example. You'll notice that std::expected is never compared. Merely, the compiler's evaluation of the constraint on operator== during overload resolution causes the infinite recursion. // mre.cpp #include <concepts> #include <expected> template <typename T> class A { public: friend bool operator==(const A&, const A&) { return true; // not using T == T; } T t; }; int main() { static_assert(std::equality_comparable<A<std::expected<int, int>>>); }; This issue is seen on GCC 15+, but not on GCC14, as the requires clause did not yet exist. Compiler output with GCC 15.1.1 compiling mre.cpp with g++ mre.cpp is shown below: In file included from mre.cpp:2: /usr/include/c++/15.1.1/expected: In substitution of ‘template<class _Up> requires !(__is_expected<_Up>) && requires(const _Tp& __t, const _Up& __u) {{__t == __u} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];} constexpr bool std::operator==(const expected<int, int>&, const _Up&) [with _Up = A<std::expected<int, int> >]’: /usr/include/c++/15.1.1/expected:1175:12: required by substitution of ‘template<class _Up> requires !(__is_expected<_Up>) && requires(const _Tp& __t, const _Up& __u) {{__t == __u} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];} constexpr bool std::operator==(const expected<int, int>&, const _Up&) [with _Up = A<std::expected<int, int> >]’ 1175 | { __t == __u } -> convertible_to<bool>; | ~~~~^~~~~~ /usr/include/c++/15.1.1/concepts:306:10: required from here 306 | { __t == __u } -> __boolean_testable; | ~~~~^~~~~~ /usr/include/c++/15.1.1/expected:1178:2: required by the constraints of ‘template<class _Tp, class _Er> template<class _Up> requires !(__is_expected<_Up>) && requires(const _Tp& __t, const _Up& __u) {{__t == __u} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];} constexpr bool std::operator==(const expected<_Tp, _Er>&, const _Up&)’ /usr/include/c++/15.1.1/expected:1174:7: in requirements with ‘const _Tp& __t’, ‘const _Up& __u’ [with _Tp = int; _Up = A<std::expected<int, int> >] /usr/include/c++/15.1.1/expected:1174:14: error: satisfaction of atomic constraint ‘requires(const _Tp& __t, const _Up& __u) {{__t == __u} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];} [with _Tp = _Tp; _Up = _Up]’ depends on itself 1174 | && requires (const _Tp& __t, const _Up& __u) { | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1175 | { __t == __u } -> convertible_to<bool>; | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1176 | } | ~ Problem Evaluation ================== I believe these evaluation steps form the issue: 1. Overload resolution occurs for operator== between two A<std::expected<int,int>> types 2. std::expected<int,int> is instantiated, along with its operator== shown above. 3. Since std::expected<int,int> is a template argument to A, namespace std is involved in ADL with std::expected<int,int>. The instantiated operator== from (2.) is then considered. 4. Although no std::expected is being compared, since constraints must be evaluated before overload resolution, the compiler evaluates the constraint on line 1175 above when considering the expected's operator==. Within the constraint, lookup is again performed for operator== between _Tp and _Up. For this MRE, _Tp is int (from std::expected<int,int>) and _Up is A<std::expected<int,int>>. 5. When looking up operator== for the evaluation of the constraint, std::expected<int,int> is again a template argument, again bringing in namespace std into scope, and recursing to step (3.) Solution ======== The std::expected's operator== above is similar to this overload from std::optional (https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/include/std/optional;h=a616dc07b1070e060ed2df92e063cc34639e9723;hb=HEAD#l1592), but std::optional does not suffer from the same issue. The difference is that std::optional's comparison operators are not defined as friends; they're defined as templated free functions. Redefining the std::expected's comparison operators as free functions solves the issue, all while maintaining the new constraints.