当参数是重载函数时,重载决策如何工作?

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

前言

C++ 中的重载解析可能是一个过于复杂的过程。要理解管理重载解析的所有 C++ 规则,需要付出相当多的脑力劳动。最近我想到参数列表中重载函数名称的存在会增加重载解析的复杂性。正好是一个广泛使用的案例,所以我发了 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 .

问题

假设一个人完全理解重载决议在没有参数的情况下是如何工作的,这些参数本身就是重载函数的名称。必须对他们对重载决议的理解进行哪些修改,以便它也涵盖将重载函数用作参数的情况?

例子

鉴于这些声明:

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)) {}

以下表达式导致以下名称解析 foo(至少对于 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)

要玩的代码:

#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)                   );
}

让我们来看最有趣的案例,

bar("abc", foo);

要弄清楚的主要问题是,要使用 bar 的哪个重载。一如既往,我们首先通过名称查找得到一组重载,然后对重载集中的每个函数模板进行模板类型推导,然后进行重载解析。

这里真正有趣的部分是声明的模板类型推导

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

标准在 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已经定义为函数模板的函数参数类型包括模板参数,所以这里Pvoid (*)(T*)。)

所以由于foo是一个包含函数模板的重载集,foovoid (*f)(T*)在模板类型推导中不起作用。这使得参数 T* x 和参数 "abc" 的类型为 const char[4]T* 不是引用,数组类型退化为指针类型 const char* 我们发现 Tconst char.

现在我们对这些候选人进行了重载决议:

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)

是时候找出其中哪些是可行的功能了。 (1)、(2) 和 (5) 不可行,因为没有从 const char[4]intdoubleA 的转换。对于 (3) 和 (4),我们需要弄清楚 foo 是否是有效的第二个参数。在标准第 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]

对于 bar 的重载 (3),我们首先尝试对

进行类型推导
template<class T> void foo(T* ) {}

目标类型 void (*)(std::string)。这失败了,因为 std::string 无法匹配 T*。但是我们发现 foo 的一个重载具有确切的类型 void (std::string),因此它在重载 (3) 情况下获胜,并且重载 (3) 是可行的。

对于bar的重载(4),我们首先尝试对相同的函数模板foo进行类型推导,这次使用目标类型void (*)(const char*)这次类型推导成功,使用T = const charfoo 的其他重载中的 None 具有确切的类型 void (const char*),因此使用了函数模板特化,并且重载 (4) 是可行的。

最后,我们通过普通重载决议比较重载 (3) 和 (4)。在这两种情况下,参数 foo 到函数指针的转换都是精确匹配,因此隐式转换序列并不比另一个更好。但是从const char[4]const char*的标准转换比从const char[4]std::string的用户自定义转换序列要好。所以 bar 的重载 (4) 是最好的可行函数(它使用 void foo<const char>(const char*) 作为参数)。