Overload resolution with std::function

In C++11…

Let’s take a look at the specification of the constructor template of std::function (which takes any Callable): [func.wrap.func.con]/7-10

template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

7 Requires: F shall be CopyConstructible. f shall be Callable (20.10.11.2) for argument types ArgTypes and return type
R. The copy constructor and destructor of A shall not throw
exceptions.

8 Postconditions: !*this if any of the following hold:

  • f is a NULL function pointer.
  • f is a NULL pointer to member.
  • F is an instance of the function class template, and !f

9 Otherwise, *this targets a copy of f initialized with std::move(f). [left out a note here]

10 Throws: shall not throw exceptions when f is a function pointer or a reference_wrapper<T> for some T. Otherwise, may throw
bad_alloc or any exception thrown by F’s copy or move constructor.

Now, constructing, or attempting to construct (for overload resolution) a std::function<void(int)> from a [](){} (i.e. with signature void(void)) violates the requirements of std::function<void(int)>‘s constructor.

[res.on.required]/1

Violation of the preconditions specified in a function’s Requires: paragraph results in undefined behavior unless the function’s Throws: paragraph specifies throwing an exception when the precondition is violated.

So, AFAIK, even the result of the overload resolution is undefined. Therefore, both versions of g++/libstdc++ are complying in this aspect.


In C++14, this has been changed, see LWG 2132. Now, the converting constructor template of std::function is required to SFINAE-reject incompatible Callables (more about SFINAE in the next chapter):

template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

7 Requires: F shall be CopyConstructible.

8 Remarks: These constructors shall not participate in overload
resolution unless f is Callable (20.9.11.2) for argument types
ArgTypes... and return type R.

[…]

The “shall not participate in overload resolution” corresponds to rejection via SFINAE. The net effect is that if you have an overload set of functions foo,

void foo(std::function<void(double)>);
void foo(std::function<void(char const*)>);

and a call-expression such as

foo([](std::string){}) // (C)

then the second overload of foo is chosen unambiguously: Since std::function<F> defines F as its interface to the outside, the F defines which argument types are passed into std::function. Then, the wrapped function object has to be called with those arguments (argument types). If a double is passed into std::function, it cannot be passed on to a function taking a std::string, because there’s no conversion double -> std::string.
For the first overload of foo, the argument [](std::string){} is therefore not considered Callable for std::function<void(double)>. The constructor template is deactivated, hence there’s no viable conversion from [](std::string){} to std::function<void(double)>. This first overload is removed from the overload set for resolving the call (C), leaving only the second overload.

Note that there’s been a slight change to the wording above, due to LWG 2420: There’s an exception that if the return type R of a std::function<R(ArgTypes...)> is void, then any return type is accepted (and discarded) for the Callable in the constructor template mentioned above. For example, both []() -> void {} and []() -> bool {} are Callable for std::function<void()>. The following situation therefore produces an ambiguity:

void foo(std::function<void()>);
void foo(std::function<bool()>);

foo([]() -> bool {}); // ambiguous

The overload resolution rules don’t try to rank among different user-defined conversions, and hence both overloads of foo are viable (first of all) and neither is better.


How can SFINAE help here?

Note when a SFINAE-check fails, the program isn’t ill-formed, but the function isn’t viable for overload resolution. For example:

#include <type_traits>
#include <iostream>

template<class T>
auto foo(T) -> typename std::enable_if< std::is_integral<T>::value >::type
{  std::cout << "foo 1\n";  }

template<class T>
auto foo(T) -> typename std::enable_if< not std::is_integral<T>::value >::type
{  std::cout << "foo 2\n";  }

int main()
{
    foo(42);
    foo(42.);
}

Similarly, a conversion can be made non-viable by using SFINAE on the converting constructor:

#include <type_traits>
#include <iostream>

struct foo
{
    template<class T, class =
             typename std::enable_if< std::is_integral<T>::value >::type >
    foo(T)
    {  std::cout << "foo(T)\n";  }
};

struct bar
{
    template<class T, class =
             typename std::enable_if< not std::is_integral<T>::value >::type >
    bar(T)
    {  std::cout << "bar(T)\n";  }
};

struct kitty
{
    kitty(foo) {}
    kitty(bar) {}
};

int main()
{
    kitty cat(42);
    kitty tac(42.);
}

Leave a Comment