通用引用和std :: initializer_list

时间:2013-06-26 08:03:52

标签: c++ c++11

在他的“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&。 至少在我看来,这种行为完全令人惊讶。 哪种行为符合标准?有谁知道背后的逻辑?

3 个答案:

答案 0 :(得分:9)

这是关键所在:从braced-init-list({expr...})中推导出类型不适用于模板参数,仅适用于auto。使用模板参数,您将获得演绎失败,并且不会考虑重载。这导致第一和第三输出。

  

无论是按initializer_list按值还是按const&

,都会产生影响

foo:对于任何X,两个带XX&参数的重载对于左值参数都不明确 - 两者都同样可行 (对于rvalues,XX&&相同)。

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&&)将是调用。所以我想这是一个含糊不清的案例。