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.

Reply via email to