C++通过返回类型重载函数

7
如果我认为自己对C++有所了解,那么我知道你不能通过返回类型来重载函数。那么请问这里发生了什么?
class A { public: typedef int _foo; };
class B {};

template<class T>
typename T::_foo Foo(int)
{
    cout << "Foo(int)\n"; return typename T::_foo();
}

template<class T>
typename T Foo(char)
{
    cout << "Foo(char)\n"; return typename T();
}

int main()
{
    Foo<A>(0);      // Writes "Foo(int)", as expected.
    Foo<B>(0);      // Writes "Foo(char), expected error unable to compile template.
    return 0;
}

有两个类A和B。A定义了typedef _foo,而B没有。函数模板Foo有两个重载版本,一个是Foo(int),另一个是Foo(char)。Foo(int)返回T ::_foo,Foo(char)返回T。

然后调用Foo(0)两次。这是Foo(int)的精确匹配,所以我希望Foo<A>(0)能够编译成功,而Foo<B>(0)不能编译成功,因为B未定义在模板中使用的类型_foo。

实际上发生的事情是Foo<B>(0)完全忽略了Foo(int),而是实例化了Foo(char)。但根据常规的重载解析规则,Foo(0)显然是Foo(int)的精确匹配,唯一使Foo(char)更具可行性的是返回类型,而不应该被考虑。

为了验证影响重载分辨率的是返回值,请添加以下内容:

template<class T>
void Bar(int)  { typename T::_foo a; cout << "Bar(int)\n"; }

template<class T>
void Bar(char) { cout << "Bar(char)\n"; }

Bar<A>(0);      // Writes "Bar(int), as expected.
//Bar<B>(0);    // Error C2039: '_foo' : is not a member of 'B', as expected.

这表明在没有返回值的情况下,Foo(int)确实是正确的重载函数,如果模板无法从其模板参数解析所使用的类型,则编译失败是正常的结果。


7
替换失败并不是一种错误。 - jrok
2
这些不是按返回类型重载的。通常情况下,这些是按参数类型重载的。 - Nawaz
2个回答

7
您并没有在返回类型上进行过载,您是在特化函数模板,并且当 Foo<B>(int) 特化形成无效类型 B::_foo 时,该特化通过 SFINAE 从重载集中删除,只留下 Foo<B>(char) 函数作为唯一可行的函数。

更详细地说,对 Foo<A>(0) 的调用首先执行名称查找以查找所有作用域内的 Foo 名称,然后实例化任何函数模板以找到重载候选项,然后重载解析选择最佳匹配项。

实例化函数模板的步骤会产生这两个函数声明:

int Foo<A>(int);
A Foo<A>(char);

过载决议选择第一个作为最佳匹配。

然而,在调用 Foo<B>(0) 时,实例化会产生以下声明:

<invalid type>  Foo<B>(int);
B Foo<B>(char);

第一个声明无效,因此只有一个重载解析的候选项,因此被调用的是那个函数。
在你的Bar示例中,在实例化期间形成的无效类型不在函数声明的“直接上下文”中(它在函数定义即主体中),因此SFINAE不适用。

3
他并没有提供函数模板的不同专业化,实际上他是在重载函数(尽管返回类型不同)。 - Mooing Duck
如果使用SFINAE,为什么Bar<B>(0)会导致编译失败而不是简单地解析为Bar(char)? - Neutrino
@MooingDuck,对的,我重新表达了一下。我的意思是Foo(int)的不同特化版本。显然,Foo(int)Foo(char)并没有根据返回类型进行重载。 - Jonathan Wakely
4
因为SFINAE只适用于函数的签名。 - Konrad Rudolph

5
template<class T>
typename T::_foo Foo(int);

template<class T>
typename T Foo(char);

所以你的代码声明了这个重载函数。很好。


Foo<A>(0);

在这种情况下,编译器会尝试填写上面声明的原型的模板,即:
int Foo(int); 
A foo(char); 

因为你将一个整数作为参数传递,所以第一个更匹配,编译器使用了它。

Foo<B>(0);

编译器再次看到这行代码并尝试为原型填充模板,但是...

WTFDOESNTMAKESENSE?!?!? Foo(int); 
A foo(char); 

很明显,第一个选项根本没有意义,因此被舍弃了并使用了第二个重载。这实际上与返回类型无关,而是与在决定你要使用哪个函数之前如何填写模板原型有关。下面是重排后的示例以澄清:

template<class T>
int foo(T::_foo) {}
template<class T>
int foo(char) {}

int main() {
    foo<A>(0); //uses the first, `int foo(int)` better than `int foo(char)`
    foo<B>(0); //uses the second, because the first doesn't work with B.

这被称为SFINAE,需要注意的是它只在模板参数、返回类型和函数参数的非常特定情况下起作用,而不是在函数体本身中。这就是为什么您的“验证”会导致错误,因为它无法从原型中确定一个函数是否无效,而原型是在选择重载时考虑的唯一因素。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接