You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
pfn::expected's comparison operators declare their expected operand by its concrete type, and constrain themselves on a comparison involving the other operand. Because that parameter is a non-deduced context, deduction can never reject a candidate on the left operand, so the constraint is evaluated for every left operand whatsoever — and when the right operand's ADL reaches the same expected, the constraint asks the question it is answering.
Both compilers reject it outright, so a question that should answer false is a hard error.
Repro (verified on main, gcc 16.1.1 and clang 22.1.6, C++20)
Nothing here involves comparing an expected with anything:
#include<pfn/expected.hpp>structP { int i; booloperator==(P const &) const = default; };
structC { int i; booloperator==(C const &) const = default; };
using E = pfn::expected<P, C>;
using G = E (*)(int); // a function pointer whose ADL reaches Etemplate <typename A, typename B>
concept eq = requires(A const &a, B const &b) { a == b; };
static_assert(not eq<P, G>); // expected: passes. actual: hard error
gcc: error: satisfaction of atomic constraint 'requires{{*x == v} -> convertible_to<bool>;}' ... depends on itself
clang: error: satisfaction of constraint 'requires { { *x == v } -> convertible_to<bool>; }' depends on itself
The same thing reaches the fn layer today, through any class template instantiated over such a type:
fn::just<G> j{nullptr}; // fine - declaring it is not the problemboolconst b = (j == j); // hard error// and asking is no better: `requires(fn::just<G> const &a) { a == a; }` hard-errors too
Mechanism
All four comparison operators in include/pfn/expected.hpp (lines 1052, 1069, 1084, 1095) have this shape:
typename Policy::template type<T, E> is a dependent qualified name, hence a non-deduced context: nothing about the left argument can make deduction fail. Constraints are checked before implicit conversion sequences are formed, so for any expression a == b whose right operand brings this hidden friend in by ADL, the constraint runs — and if b's type also brings it back in, satisfaction depends on itself.
The trigger therefore needs only:
a right operand whose associated entities include the expected — a function pointer or reference returning one, or a class template instantiated over one (fn::just<G>, fn::pack<G>), and
a left operand that is not itself that expected.
Why std::expected does not have this
[expected.object.eq] specifies *x == v as a Mandates, not a Constraint, so a letter-conforming implementation never evaluates it during overload resolution. pfn promotes it to a constraint (the source marks the neighbouring noexcept "extension"), which is a real improvement — asking answers instead of hard-erroring in the body — and libstdc++ makes the same choice. But libstdc++ pairs it with a deduced left operand:
so a left operand that is not an expected fails deduction, and the candidate dies before the recursive requirement is ever reached. Measured side by side in the same TU shape:
ask
pfn::expected
std::expected (libstdc++)
expected == same expected
1
1
expected == other expected
1
1
expected == value
1
1
expected == unexpected
1
1
value == expected (reversed)
1
1
value == fn-pointer returning that expected
hard error
0
convertible-to-value == fn-pointer returning that expected
hard error
0
value == unrelated struct
0
0
Every intended comparison already works; only the questions that should answer false are poisoned.
Proposed fix
Adopt libstdc++'s pairing: keep the constraint, make the expected operand deducible, so deduction rejects a non-expected left operand before the constraint is evaluated. With the Policy indirection the parameter cannot be spelled as a deducible expected<V, E>, so the deduced parameter has to be the operand itself:
Associated constraints are a conjunction checked in order, so the same_as test short-circuits before the self-referential requirement. The same treatment belongs on all four operators for consistency, whether or not each is reachable today.
Notes
unexpected's operator== is not implicated: the failures above attribute to the operator at line 1084.
This blocks the "defaulted operator== and operator<=> on pack" item of Minor issues #380 — a defaulted comparison forces exactly this question at class-instantiation time, for every element type, and fn::pack is what every verb functor stores its callables in.
Worth a WG21 look: an implementation that reads the Mandates as an invitation to constrain (as both pfn and libstdc++ do) gets this unless it also deduces the operand.
pfn::expected's comparison operators declare their expected operand by its concrete type, and constrain themselves on a comparison involving the other operand. Because that parameter is a non-deduced context, deduction can never reject a candidate on the left operand, so the constraint is evaluated for every left operand whatsoever — and when the right operand's ADL reaches the sameexpected, the constraint asks the question it is answering.Both compilers reject it outright, so a question that should answer
falseis a hard error.Repro (verified on
main, gcc 16.1.1 and clang 22.1.6, C++20)Nothing here involves comparing an
expectedwith anything:The same thing reaches the
fnlayer today, through any class template instantiated over such a type:fn::just<G> j{nullptr}; // fine - declaring it is not the problem bool const b = (j == j); // hard error // and asking is no better: `requires(fn::just<G> const &a) { a == a; }` hard-errors tooMechanism
All four comparison operators in
include/pfn/expected.hpp(lines 1052, 1069, 1084, 1095) have this shape:typename Policy::template type<T, E>is a dependent qualified name, hence a non-deduced context: nothing about the left argument can make deduction fail. Constraints are checked before implicit conversion sequences are formed, so for any expressiona == bwhose right operand brings this hidden friend in by ADL, the constraint runs — and ifb's type also brings it back in, satisfaction depends on itself.The trigger therefore needs only:
expected— a function pointer or reference returning one, or a class template instantiated over one (fn::just<G>,fn::pack<G>), andexpected.Why
std::expecteddoes not have this[expected.object.eq] specifies
*x == vas a Mandates, not a Constraint, so a letter-conforming implementation never evaluates it during overload resolution.pfnpromotes it to a constraint (the source marks the neighbouringnoexcept"extension"), which is a real improvement — asking answers instead of hard-erroring in the body — and libstdc++ makes the same choice. But libstdc++ pairs it with a deduced left operand:so a left operand that is not an
expectedfails deduction, and the candidate dies before the recursive requirement is ever reached. Measured side by side in the same TU shape:pfn::expectedstd::expected(libstdc++)expected == same expectedexpected == other expectedexpected == valueexpected == unexpectedvalue == expected(reversed)value == fn-pointer returning that expectedconvertible-to-value == fn-pointer returning that expectedvalue == unrelated structEvery intended comparison already works; only the questions that should answer
falseare poisoned.Proposed fix
Adopt libstdc++'s pairing: keep the constraint, make the expected operand deducible, so deduction rejects a non-expected left operand before the constraint is evaluated. With the
Policyindirection the parameter cannot be spelled as a deducibleexpected<V, E>, so the deduced parameter has to be the operand itself:Associated constraints are a conjunction checked in order, so the
same_astest short-circuits before the self-referential requirement. The same treatment belongs on all four operators for consistency, whether or not each is reachable today.Notes
unexpected'soperator==is not implicated: the failures above attribute to the operator at line 1084.operator==andoperator<=>onpack" item of Minor issues #380 — a defaulted comparison forces exactly this question at class-instantiation time, for every element type, andfn::packis what every verb functor stores its callables in.pfnand libstdc++ do) gets this unless it also deduces the operand.