Document number: P0826R0
Date: 2017-10-12
Project: Programming Language C++, Library Evolution Working Group
Reply-to: Agustín Bergé agustinberge@gmail.com
SFINAE-friendly std::bind
Tension Between SFINAE and Deduced Return Types
1. Introduction
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.
2. Discussion
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::is_invocable
, std::result_of
, 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:
// SFINAE-unfriendly callable struct sfinae_unfriendly { template < typename First, typename ...Rest> struct returns { static_assert ( ! std::is_floating_point_v <First>, "First argument can't be a floating point type" ); using type = First; }; template < typename ...Args> auto operator ()(Args&&... args) const -> typename returns<Args...>::type { /*...*/ } }; // SFINAE-friendly call wrapper template < typename F> class call_wrapper { F _f; public : call_wrapper(F f) : _f( std::move (f)) {} template < typename ...Args> auto operator ()(Args&&... args) const // const prepends an int -> std::invoke_result_t <F const &, int , Args...> { return _f(1, std::forward <Args>(args)...); } template < typename ...Args> auto operator ()(Args&&... args) volatile // volatile prepends a double -> std::invoke_result_t <F volatile &, double , Args...> { return _f(1.0, std::forward <Args>(args)...); } }; // attempt to call const-qualified operator(), // which calls sfinae_unfriendly with int argument call_wrapper const w = sfinae_unfriendly{}; w(); // error: static assertion failed // static_assert(!std::is_floating_point_v<First>) // required by substitution of // std::invoke_result_t<sfinae_unfriendly volatile &, double, Args...> // call_wrapper<sfinae_unfriendly>::operator()<Args...>(Args&&...) volatile // [with Args = {}]' |
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
return
statement 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++:
int i; std::bind ([]( auto & x) -> void { x = 1; }, i)(); // fine std::bind ([]( auto & x) { x = 1; }, i)(); // error: assignment of read-only reference 'x' // required by substitution of // Result std::_Bind<lambda(auto&), int>::operator()<Args..., Result>(Args&&...) const // [with Args = {}; Result = <missing>] |
[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:
// SFINAE-unfriendly call wrapper template < typename F> class call_wrapper { /*...*/ template < typename ...Args> decltype ( auto ) operator ()(Args&&... args) const // const prepends an int { return _f(1, std::forward <Args>(args)...); } template < typename ...Args> decltype ( auto ) operator ()(Args&&... args) volatile // volatile prepends a double { return _f(1.0, std::forward <Args>(args)...); } }; call_wrapper const w = sfinae_unfriendly{}; w(); // fine, calls const-qualified operator() |
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 not_fn
—.
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:
struct fun { template < typename ...Args> void operator ()(Args&&... args) = delete ; template < typename ...Args> bool operator ()(Args&&... args) const { return true ; } }; std::bind (fun{})(); // error: call to deleted function |
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:
std::not_fn (fun{})(); // fine, returns false |
3. Implementation Experience
libc++: SFINAE-friendly, as a result of this issue report.
libstdc++: SFINAE-friendly for
std::bind
but notstd::bind<R>
, unintentional?MSVC: Not SFINAE-friendly, triggers diagnostic in
std::tuple_element
when there are not enough unbound arguments to satisfy all placeholders.
All three implementations yield a compilation error for the following snippet:
std::bind ([]( auto & x) -> void { x = 1; }, 42)(); // fine std::bind ([]( auto & x) { x = 1; }, 42)(); // error |
- 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:
std::bind ([]( auto & x) -> void { x = 1; }, 42)(); // fine std::bind < void >([]( auto & x) { x = 1; }, 42)(); // error |
4. References
-
[N4687] ISO/IEC JTC1 SC22 WG21, Programming Languages - C++, working draft, July 2017
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4687.pdf -
[N3462]
std::result_of
and SFINAE - Eric Niebler, et. al.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3462.html -
[P0077]
is_callable
, the missing INVOKE related trait - Agustín Bergé
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0077r2.html -
[P0358] Fixes for
not_fn
- Tomasz Kamiński
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0358r1.html