Document number: P0826R0
Project: Programming Language C++, Library Evolution Working Group
Reply-to: Agustín Bergé email@example.com
Tension Between SFINAE and Deduced Return Types
This paper proposes mandating that the call wrapper returned from
std::bind be SFINAE-friendly, and explores the implications a SFINAE-friendly call wrapper has for deduced return types on the target callable.
In a nutshell, bind expressions are not SFINAE-friendly. As a consequence, they do not play nice with
std::is_invocable; simply asking the question may result in the program being ill-formed. Implementations diverge in their level of SFINAE-friendliness, resulting in a poor user experience when
std::bind is mixed with facilities built on top of SFINAE like
std::function, and more recently concepts and constraints.
A SFINAE-friendly bind expression implementation requires checking that there are sufficient unbound arguments to fulfill all placeholders, and either constraining all function call operators on the well-formedness of their corresponding call expressions, or performing any return type computation in an immediate context, such that substitution failures do not render the program ill-formed.
2.1 On Poisonous Overloads
When target callables that are not SFINAE-friendly are used together with a SFINAE-friendly call wrapper the result is, unsurprisingly, a SFINAE-unfriendly callable. There is a more subtle interaction, however, in that different cv/ref-qualified function call operator overloads involve slightly different call expressions each, and any one of them could render the program ill-formed, regardless of the well-formedness of the others. In a worst case scenario, these poisonous overloads may cause the wrapping of a SFINAE-unfriendly callable within a SFINAE-friendly call wrapper to result in a callable which is ill-formed for every call expression:
This is not a new problem, but it is becoming a more common one. Deduced return types are notably SFINAE-unfriendly:
10.1.7.4 [dcl.spec.auto]/10 Return type deduction for a function template with a placeholder in its declared type occurs when the definition is instantiated even if the function body contains a
returnstatement with a non-type-dependent operand. [Note: Therefore, any use of a specialization of the function template will cause an implicit instantiation. Any errors that arise from this instantiation are not in the immediate context of the function type and can result in the program being ill-formed. —end note] [...]
As modern C++ practices push for deduced return types, SFINAE-based facilities suffer. There's tension between them, and something's gotta give...
2.2 On Poisoned bind expressions
The following similar scenario has been reported as a bug against libstdc++ and libc++:
[Note: It may seem that this snippet attempts to modify
i, but it actually attempts to modify a bound argument initialized from it; this is only viable for a non-const bind expression. The const-qualified operator overload poisons the call in the deduced return type case. —end note]
The reports further claim that the above failure is due to non-conforming implementations, caused by their SFINAE-friendliness —be it deliberate or accidental—. Instead, it has been suggested that bind expressions in particular and forwarding call wrappers in general should be mandated to NOT be SFINAE-friendly, so that they may themselves be implemented using deduced return types:
This paper does not consider such approach an adequate solution to the problem. Instead, it proposes to continue the SFINAE-friendly trend that started with N3462 —
std::result_of and SFINAE— and gave rise to P0077 —
is_callable, the missing INVOKE related trait—; trend that has been reconfirmed in P0358 —Fixes for
2.3 On Deleted Overloads
A SFINAE-friendly implementation has to be careful to not fall back to a const-qualified overload when the non-const invoke expression is not well-formed. A naive implementation might fail to correctly propagate the cv-qualifiers of the call wrapper, causing it to accept the following ill-formed snippet:
It should be noted that this differs from the required behavior for
std::not_fn —as specified in the current working draft N4687—, which might be considered a defect:
3. Implementation Experience
libc++: SFINAE-friendly, as a result of this issue report.
libstdc++: SFINAE-friendly for
MSVC: Not SFINAE-friendly, triggers diagnostic in
std::tuple_elementwhen there are not enough unbound arguments to satisfy all placeholders.
All three implementations yield a compilation error for the following snippet:
- Boost: Not SFINAE-friendly, return type computation relies on nested
result_type; all call expressions appear well-formed in unevaluated contexts.
Only libc++, the SFINAE-friendly implementation, yields a compilation error for the following snippet:
[N4687] ISO/IEC JTC1 SC22 WG21, Programming Languages - C++, working draft, July 2017
std::result_ofand SFINAE - Eric Niebler, et. al.
is_callable, the missing INVOKE related trait - Agustín Bergé
[P0358] Fixes for
not_fn- Tomasz Kamiński