当参数是重载函数时,重载解析是如何工作的

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

本文关键字:重载 何工作 工作 函数 参数      更新时间:2023-10-16

序言

C++中的重载解析可能是一个过于复杂的过程。要理解所有控制重载解析的C++规则,需要花费大量的脑力劳动。最近我突然想到,参数列表中出现重载函数的名称会增加重载解析的复杂性。由于这恰好是一个广泛使用的案例,我发布了一个问题并得到了一个答案,这让我更好地理解了这个过程的机制。然而,在iostreams的背景下对这个问题的表述似乎在一定程度上分散了答案的焦点,使其偏离了所解决问题的本质。因此,我开始深入研究,并提出了其他例子,要求对这个问题进行更详细的分析。这个问题是一个介绍性的问题,后面是一个更复杂的问题。

问题

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

示例

鉴于这些声明:

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:中有这样的表述

P是函数类型、指向函数类型的指针或指向成员函数类型的指示器时:

  • 如果参数是包含一个或多个函数模板的重载集,则该参数将被视为非推导上下文。

  • 如果参数是重载集(不包含函数模板(,则会尝试使用该集的每个成员进行试参数推导。如果只有一个重载集成员的推导成功,则该成员将用作推导的参数值。如果重载集的多个成员的推导成功,则参数将被视为非推导上下文。

(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节:

在某些上下文中,使用不带参数的重载函数名会解析为重载集中特定函数的函数、指向函数的指针或指向成员函数的指针。函数模板名称被认为是在这种上下文中命名一组重载函数。所选函数的类型与上下文中所需目标类型的函数类型相同。目标可以是

  • 函数(5.2.2(的参数

如果名称是函数模板,则完成模板参数推导(14.8.2.2(,如果参数推导成功,则生成的模板参数列表将用于生成单个函数模板专用化,该专用化将添加到所考虑的重载函数集中。。。

[注意:如果f()g()都是重载函数,则必须考虑可能性的叉积来解析f(&g)或等效表达式f(g)。-结束注释]

对于bar的过载(3(,我们首先尝试的类型推导

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

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

对于bar的重载(4(,我们首先尝试对相同的函数模板foo进行类型推导,这次是目标类型void (*)(const char*)。这次类型推导成功,T=const charfoo的其他重载都没有确切的类型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*)作为其自变量(。