Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overload resolution with std::function

Consider this example of code:

#include <iostream>
#include <functional>

typedef std::function<void()> func1_t;
typedef std::function<void(int)> func2_t;

struct X
{
   X (func1_t f)
   { }

   X (func2_t f)
   { }
};

int main ( )
{
   X x([](){ std::cout << "Hello, world!\n"; });
}

I was sure that it shouldn't compile, because the compiler shouldn't be able to choose one of the two constructors. g++-4.7.3 shows this expected behavior: it says that call of overloaded constructor is ambiguous. However, g++-4.8.2 successfully compiles it.

Is this code correct in C++11 or it is a bug/feature of this version of g++?

like image 371
lisyarus Avatar asked Mar 03 '14 12:03

lisyarus


People also ask

How do a function call gets matched to a set of overloaded functions?

When a function call is made to an overloaded function, the compiler steps through a sequence of rules to determine which (if any) of the overloaded functions is the best match. At each step, the compiler applies a bunch of different type conversions to the argument(s) in the function call.

What is overload resolution?

The process of selecting the most appropriate overloaded function or operator is called overload resolution. Suppose that f is an overloaded function name. When you call the overloaded function f() , the compiler creates a set of candidate functions.

How do you create an overloaded function in C++?

You overload a function name f by declaring more than one function with the name f in the same scope. The declarations of f must differ from each other by the types and/or the number of arguments in the argument list.

How do you fix an ambiguous call to overloaded function?

There are two ways to resolve this ambiguity: Typecast char to float. Remove either one of the ambiguity generating functions float or double and add overloaded function with an int type parameter.


1 Answers

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.);
}
like image 195
dyp Avatar answered Sep 18 '22 12:09

dyp