在他的“C ++ and Beyond 2012:Universal References”演示中,Scott反复强调了一点,即通用引用处理/绑定到所有内容,从而重载已经采用通用引用参数的函数没有意义。
在我用std::initializer_list
混合它们之前,我没有理由怀疑它。
这是一个简短的例子:
#include <iostream>
#include <initializer_list>
using namespace std;
template <typename T>
void foo(T&&) { cout << "universal reference" << endl; }
template <typename T>
void foo(initializer_list<T>) { cout << "initializer list" << endl; }
template <typename T>
void goo(T&&) { cout << "universal reference" << endl; }
template <typename T>
void goo(initializer_list<T> const&) { cout << "initializer list" << endl; }
int main(){
auto il = {4,5,6};
foo( {1,2,3} );
foo( il );
goo( {1,2,3} );
goo( il );
return 0;
}
奇怪的是,VC11 2012年11月CTP抱怨模棱两可(error C2668: 'foo' : ambiguous call to overloaded function
)。更令人惊讶的是,gcc-4.7.2,gcc-4.9.0和clang-3.4同意以下输出:
initializer list
initializer list
initializer list
universal reference
所以显然有可能(使用gcc和clang)重载使用initializer_list
s的通用引用的函数,但是当使用auto + { expr } => initializer_list
时,使用initializer_list
也就是{{1} }按值或按const&
。
至少在我看来,这种行为完全令人惊讶。
哪种行为符合标准?有谁知道背后的逻辑?
答案 0 :(得分:9)
这是关键所在:从braced-init-list({expr...}
)中推导出类型不适用于模板参数,仅适用于auto
。使用模板参数,您将获得演绎失败,并且不会考虑重载。这导致第一和第三输出。
无论是按
,都会产生影响initializer_list
按值还是按const&
foo
:对于任何X
,两个带X
和X&
参数的重载对于左值参数都不明确 - 两者都同样可行 (对于rvalues,X
与X&&
相同)。
struct X{};
void f(X);
void f(X&);
X x;
f(x); // error: ambiguous overloads
然而,部分排序规则在这里(第14.5.6.2节)中进行,而采用通用std::initializer_list
的函数比通用的更加专业化。
goo
:对于两个带有X&
和X const&
参数以及X&
参数的重载,第一个是更可行,因为第二个过载需要从X&
到X const&
的资格转换(§13.3.3.1.2/ 1表12和§13.3.3.2/ 3第三子项)。
答案 1 :(得分:4)
如果斯科特真的说他错了,那就是他教导的误导性“普遍参照”心理模型的另一个问题。
所谓的“通用引用” 贪婪,并且可能在您不希望或期望它们时匹配,但这并不意味着它们始终是最佳匹配。
非模板重载可以完全匹配,并且优先于“通用引用”,例如这会选择非模板
bool f(int) { return true; }
template<typename T> void f(T&&) { }
bool b = f(0);
模板重载可以比“通用引用”更专业,因此将通过重载决策来选择。 e.g。
template<typename T> struct A { };
template<typename T> void f(T&&) { }
template<typename T> bool f(A<T>) { return true; }
bool b = f(A<int>());
DR 1164确认即使f(T&)
比f(T&&)
更专业,也更适合左值。
在你的两个案例中,initializer_list
重载不仅更加专业化,而且{1,2,3}
这样的braced-init-list永远不会被模板参数推断推断出来。
您的结果的解释是:
foo( {1,2,3} );
您无法从braced-init-list中推断出模板参数,因此foo(T&&)
的推论失败,而foo(initializer_list<int>)
是唯一可行的函数。
foo( il );
foo(initializer_list<T>)
比foo(T&&)
更专业,因此通过重载解析来选择。
goo( {1,2,3} );
您无法从braced-init-list中推断出模板参数,因此goo(initializer_list<int>)
是唯一可行的函数。
goo( il );
il
是一个非常数左值,goo(T&&)
可以使用推导为T
的{{1}}进行调用,因此其签名为initializer_list<int>&
,这是更好的匹配比goo(initializer_list<int>&)
,因为将非const goo(initializer_list<int> const&)
绑定到const-reference是一个比将其绑定到非const-reference更糟糕的转换序列。
上述评论之一引用斯科特的幻灯片说:“没有意义:URef处理所有事情。”这是真的,这正是你想要超载的原因!对于某些类型,您可能需要更具体的函数,对于其他所有类型,您可能需要通用引用函数。您还可以使用SFINAE来约束通用引用函数以阻止它处理某些类型,以便其他重载可以处理它们。
对于标准库中的示例,il
是一个带有通用引用的重载函数。一个重载处理第一个参数类型为std::async
而另一个重载处理其他所有内容的情况。 SFINAE通过贪婪地匹配将std::launch
作为第一个参数传递的调用来防止“其他所有”重载。
答案 2 :(得分:0)
好的,首先对foo
的反应是有道理的。 initializer_list<T>
匹配两个调用并且更专业,因此应该以这种方式调用。
对于goo
,这与完美转发同步。在调用goo(il)
时,可以选择goo(T&&)
(带T = initializer_list<T>&
)和常量参考版本。我想使用非const引用调用版本优先于使用const引用的更专业版本。话虽如此,我不确定这是一个定义明确的情况w.r.t.标准。
修改强>
请注意,如果没有模板,则可以通过标准的段落13.3.3.2(排名隐式转换序列)来解决。这里的问题是,AFAIK,模板函数的部分排序将指示要调用的第二个(更专业的)goo(initializer_list<T> const&)
,但隐式转换序列的排名将指示goo(T&&)
将是调用。所以我想这是一个含糊不清的案例。