Deduce the expected operand of the comparison against a value - #382
Conversation
The operator constrains itself on the other operand, which is only safe where its own operand is deduced: as a hidden friend it could be spelled only as Policy::type<T, E>, a non-deduced context, so deduction rejected nothing and the constraint ran for every left operand there is. Where that operand reached this same operator by ADL, satisfaction depended on itself - a hard error, where the question should simply answer false. Declared at namespace scope instead, once per carrier; its three siblings constrain on the operands' channels rather than on the other operand, and keep their form. Closes #381 Assisted-by: Claude:claude-opus-5
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
🤖 Augment PR SummarySummary: This PR fixes a hard-error recursion bug in Changes:
Technical Notes: The fix prevents constraints from being evaluated for unrelated left operands, avoiding self-dependent constraint satisfaction when ADL reaches the same operator. 🤖 Was this summary useful? React with 👍 or 👎 |
|
|
On record:
So we can have "hidden friend" (as mandated by standard) or "constrained on the other operand", but not both — at least not when the class is reached through a policy indirection that makes its own name unspellable. We choose the latter, because it provides better user experience. Note, libstdc++ also constraints rather than mandates on the other operand - so we are not alone. |
The operator constrains itself on the other operand, which is only safe where its own operand is deduced: as a hidden friend it could be spelled only as Policy::type<T, E>, a non-deduced context, so deduction rejected nothing and the constraint ran for every left operand there is. Where that operand reached this same operator by ADL, satisfaction depended on itself - a hard error, where the question should simply answer false. Declared at namespace scope instead, once per carrier; its three siblings constrain on the operands' channels rather than on the other operand, and keep their form. Closes #381 Assisted-by: Claude:claude-opus-5



Fixes #381 —
pfn::expected's comparison against a value constrained itself on the other operand while spelling its own operand through the policy, a non-deduced context. Deduction could therefore reject nothing, so the constraint was evaluated for every left operand in the program; where the right operand reached the same operator by ADL, satisfaction depended on itself, which both compilers reject outright.Verified on
mainbefore the fix, gcc 16.1.1 and clang 22.1.6:and at the
fnlayer, where it was reachable through any class template over such a type — comparingfn::just<E (*)(int)>, or merely asking whether it compares, was a hard error.The change
The operator is moved out of
_expected_baseto namespace scope, once per carrier (pfnforpfn::expected,fnforfn::expected), where its operand is deduced from its own class template. A left operand which is not thatexpectedthen fails deduction, and the constraint is never reached. ADL still finds it: associated entities include base classes regardless of access, which is how both carriers reached the hidden friends through their private base.Three things ruled out along the way, each for a concrete reason:
Selfparameter (requires same_as<Self, Policy::type<T, E>>) works on gcc and clang but MSVC rejects it withC2995: function template has already been defined— the parameter list no longer mentions the enclosing class'sT/E/Policy, so every instantiation of_expected_basedeclares the same signature. gcc and clang tell them apart by the requires-clause, which [defns.signature.templ] includes through the template-head.*x == vis a Mandates, not a Constraint): that is what makes the standard's own hidden friend safe, but it would make asking about an invalid comparison a hard error inside the body instead of an answer — and it would diverge from libstdc++, which constrains exactly aspfndoes and pairs it with a deduced operand for exactly this reason.The other three comparison operators are unchanged. They constrain on the operands' channels (
*x == *y,x.error() == y.error()), never on the other operand's type, so they cannot ask a question that re-enters them. Measured after fixing this one alone,pfnandstd::expectednow agree on every row of the same battery — including the rows that must answerfalse:std::expectedexpected == same expected/other expected/value/unexpected/ reversedvalue == fn-pointer returning that expectedconvertible-to-value == fn-pointer returning that expectedvalue == unrelated structTests
tests/pfn/expected.cpp— a section pinning that a left operand which is not the expected answers, with the positive controls that the comparison it exists for still applies. This file is re-compiled againststd::expectedbyexpected_validation.cppunderVALIDATE_CXX23, so these pins assert thatpfnandstdanswer identically.tests/fn/just.cpp— thefn-layer witness:just<F>whereFreturns anexpectednow compares.Verification
Six-config gate (gcc/clang × Debug/Release/validate) green, C++26 mode green (86/86), dev 43/43, and MSVC 17 green.
🤖 Generated with Claude Code
https://claude.ai/code/session_0198ikYYxtCqwNG54SS1b249