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 N3462std::result_of and SFINAE— and gave rise to P0077is_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 not std::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