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.