C ++中的过载分辨率可能是一个过于复杂的过程。理解控制重载决策的所有C ++规则需要花费大量精力。最近我发现在参数列表中存在重载函数的名称会增加重载决策的复杂性。由于它恰好是一个广泛使用的案例,我发布了a question并得到了一个答案,这使我能够更好地理解该过程的机制。然而,在iostreams的背景下提出这个问题似乎有点分散了答案的焦点,从正在解决的问题的本质。所以我开始深入研究并提出了其他要求对问题进行更详细分析的例子。这个问题是一个介绍性的问题,然后是more sophisticated one。
假设一个人完全理解重载解析如何在没有自身名称的重载函数的情况下工作。必须对他们对重载决策的理解做出哪些修改,以便它还包括使用重载函数作为参数的情况?
鉴于这些声明:
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) );
}
答案 0 :(得分:10)
让我们来看看最有趣的案例,
bar("abc", foo);
要弄清楚的主要问题是,要使用bar
的重载。与往常一样,我们首先通过名称查找获得一组重载,然后对重载集中的每个函数模板进行模板类型推导,然后执行重载解析。
这里真正有趣的部分是声明的模板类型推导
template<class T> void bar(T* x, void (*f)(T*)) {}
标准在14.8.2.1/6中有这样的说法:
当
P
是函数类型时,指向函数类型的指针或指向成员函数类型的指针:
如果参数是包含一个或多个函数模板的重载集,则该参数将被视为非推导的上下文。
如果参数是重载集(不包含函数模板),则尝试使用集合中的每个成员进行试验参数推导。如果仅对其中一个重载集成员进行推导成功,则该成员将用作推导的参数值。如果对重载集的多个成员进行推导成功,则该参数将被视为非推导上下文。
(P
已被定义为函数模板的函数参数类型,包括模板参数,因此P
为void (*)(T*)
。)
因为foo
是一个包含函数模板的重载集,foo
和void (*f)(T*)
在模板类型推导中不起作用。这使得参数T* x
和参数"abc"
的类型为const char[4]
。 T*
不是引用,数组类型衰减为指针类型const char*
,我们发现T
为const 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]
到int
,double
或A
的转换。对于(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* ) {}
目标类型为void (*)(std::string)
。由于std::string
无法与T*
匹配,因此失败。但是我们发现foo
的一个重载具有确切的类型void (std::string)
,因此它会在重载(3)情况下获胜,而重载(3)是可行的。
对于bar
的重载(4),我们首先尝试对相同的函数模板foo
进行类型推导,这次使用目标类型void (*)(const char*)
这次类型推导成功,{{ 1}} = T
。 const char
的其他任何重载都没有确切的类型foo
,因此使用了函数模板特化,而过载(4)是可行的。
最后,我们通过普通的重载分辨率比较重载(3)和(4)。在这两种情况下,参数void (const char*)
到指向函数的指针的转换是完全匹配,因此隐式转换序列都不比另一个好。但是,从foo
到const char[4]
的标准转换优于用户定义的从const char*
到const char[4]
的转换序列。所以std::string
的重载(4)是最好的可行函数(它使用bar
作为参数)。