Leon Leon - 1 month ago 16
C++ Question

How does overload resolution work when an argument is an overloaded function?

Preamble



Overload resolution in C++ can be an overly complex process. It takes quite a lot of mental effort to understand all of the C++ rules that govern overload resolution. Recently it occurred to me that the presence of the name of an overloaded function in the argument list can add to the complexity of overload resolution. Since it happened to be a widely used case, I posted a question and received an answer that allowed me to better understand the mechanics of that process. However, the formulation of that question in the context of iostreams seems to have somewhat distracted the focus of the answers from the very essence of the problem being addressed. So I started delving deeper and came up with other examples that ask for more elaborate analysis of the issue. This question is an introductory one and is followed by a more sophisticated one.

Question



Assume that one fully understands how overload resolution works in the absence of arguments that are themselves names of overloaded functions. What amendments must be made to their understanding of overload resolution, so that it also covers cases where overloaded functions are used as arguments?

Examples



Given these declarations:

void foo(int) {}
void foo(double) {}
void foo(std::string) {}
template<class T> void foo(T* ) {}

struct A {
A(void (*)(int)) {}
};

void bar(int x, void (*f)(int)) {}
void bar(double x, void (*f)(double)) {}
void bar(std::string x, void (*f)(std::string)) {}
template<class T> void bar(T* x, void (*f)(T*)) {}
void bar(A x, void (*f2)(double)) {}


Below expressions result in the following resolution of the name
foo
(at least with gcc 5.4):

bar(1, foo); // foo(int)
// but if foo(int) is removed, foo(double) takes over

bar(1.0, foo); // foo(double)
// but if foo(double) is removed, foo(int) takes over

int i;
bar(&i, foo); // foo<int>(int*)

bar("abc", foo); // foo<const char>(const char*)
// but if foo<T>(T*) is removed, foo(std::string) takes over

bar(std::string("abc"), foo); // foo(std::string)

bar(foo, foo); // 1st argument is foo(int), 2nd one - foo(double)





Code to play with:



#include <iostream>
#include <string>

#define PRINT_FUNC std::cout << "\t" << __PRETTY_FUNCTION__ << "\n";

void foo(int) { PRINT_FUNC; }
void foo(double) { PRINT_FUNC; }
void foo(std::string) { PRINT_FUNC; }
template<class T> void foo(T* ) { PRINT_FUNC; }

struct A { A(void (*f)(int)){ f(0); } };

void bar(int x, void (*f)(int) ) { f(x); }
void bar(double x, void (*f)(double) ) { f(x); }
void bar(std::string x, void (*f)(std::string)) { f(x); }
template<class T> void bar(T* x, void (*f)(T*)) { f(x); }
void bar(A, void (*f)(double)) { f(0); }

#define CHECK(X) std::cout << #X ":\n"; X; std::cout << "\n";

int main()
{
int i = 0;
CHECK( bar(i, foo) );
CHECK( bar(1.0, foo) );
CHECK( bar(1.0f, foo) );
CHECK( bar(&i, foo) );
CHECK( bar("abc", foo) );
CHECK( bar(std::string("abc"), foo) );
CHECK( bar(foo, foo) );
}

Answer

Let's take the most interesting case,

bar("abc", foo);

The primary question to figure out is, which overload of bar to use. As always, we first get a set of overloads by name lookup, then do template type deduction for each function template in the overload set, then do overload resolution.

The really interesting part here is the template type deduction for the declaration

template<class T> void bar(T* x, void (*f)(T*)) {}

The Standard has this to say in 14.8.2.1/6:

When P is a function type, pointer to function type, or pointer to member function type:

  • If the argument is an overload set containing one or more function templates, the parameter is treated as a non-deduced context.

  • If the argument is an overload set (not containing function templates), trial argument deduction is attempted using each of the members of the set. If deduction succeeds for only one of the overload set members, that member is used as the argument value for the deduction. If deduction succeeds for more than one member of the overload set the parameter is treated as a non-deduced context.

(P has already been defined as the function template's function parameter type including template parameters, so here P is void (*)(T*).)

So since foo is an overload set containing a function template, foo and void (*f)(T*) don't play a role in template type deduction. That leaves parameter T* x and argument "abc" with type const char[4]. T* not being a reference, the array type decays to a pointer type const char* and we find that T is const char.

Now we have overload resolution with these candidates:

void bar(int x, void (*f)(int)) {}                             // (1)
void bar(double x, void (*f)(double)) {}                       // (2)
void bar(std::string x, void (*f)(std::string)) {}             // (3)
void bar<const char>(const char* x, void (*f)(const char*)) {} // (4)
void bar(A x, void (*f2)(double)) {}                           // (5)

Time to find out which of these are viable functions. (1), (2), and (5) are not viable because there is no conversion from const char[4] to int, double, or A. For (3) and (4) we need to figure out if foo is a valid second argument. In Standard section 13.4/1-6:

A use of an overloaded function name without arguments is resolved in certain contexts to a function, a pointer to function or a pointer to member function for a specific function from the overload set. A function template name is considered to name a set of overloaded functions in such contexts. The function selected is the one whose type is identical to the function type of the target type required in the context. The target can be

  • ...
  • a parameter of a function (5.2.2),
  • ...

... If the name is a function template, template argument deduction is done (14.8.2.2), and if the argument deduction succeeds, the resulting template argument list is used to generate a single function template specialization, which is added to the set of overloaded functions considered. ...

[Note: If f() and g() are both overloaded functions, the cross product of possibilities must be considered to resolve f(&g), or the equivalent expression f(g). - end note]

For overload (3) of bar, we first attempt type deduction for

template<class T> void foo(T* ) {}

with target type void (*)(std::string). This fails since std::string cannot match T*. But we find one overload of foo which has the exact type void (std::string), so it wins for the overload (3) case, and overload (3) is viable.

For overload (4) of bar, we first attempt type deduction for the same function template foo, this time with target type void (*)(const char*) This time type deduction succeeds, with T = const char. None of the other overloads of foo have the exact type void (const char*), so the function template specialization is used, and overload (4) is viable.

Finally, we compare overloads (3) and (4) by ordinary overload resolution. In both cases, the conversion of argument foo to a pointer to function is an Exact Match, so neither implicit conversion sequence is better than the other. But the standard conversion from const char[4] to const char* is better than the user-defined conversion sequence from const char[4] to std::string. So overload (4) of bar is the best viable function (and it uses void foo<const char>(const char*) as its argument).