通用引用和std :: initializer_list

14
在他的“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 Nov 2012 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,但是当使用 auto + { expr } => initializer_list 这种语法时,无论是按值传递还是按const&传递initializer_list都没有区别。 至少对我来说,这种行为完全出乎意料。 哪种行为符合标准?有人知道背后的逻辑吗?


最后一个是 [over.ics.rank]/3 子条款 6 的例子,说明参数类型较少 cv 限定的重载将被明确使用。 - dyp
2
你确定演示文稿中有像你所声称的那样通用的语句吗?虽然通用引用可以绑定到任何东西,但很明显过载消歧机制(对于模板而言)并不总是将通用引用排名为最具体的重载。 - jogojapan
快速翻阅幻灯片,在第17页上写着:重载 + URef 几乎总是错误的。没有意义:URefs 可以处理所有东西。 [...] - user2523017
1
@user2523017 好的。用于排列模板重载的规则并不容易理解。我可能是错的(尽管Xeo的答案似乎同意),但我本能地认为template <class T> void f(initializer_list<T>)template <class T> void f(T&&)更加专业化。使用GCC进行简单测试也证实了这一点(不一定要使用initializer_list作为容器)。 - jogojapan
@DyP 我可能误解了你所说的部分内容,但我有一种怀疑,你混淆了普通函数重载决议(§13.3)和函数模板重载决议(§14.5.6.2)。如果一个函数模板与非模板函数竞争,这两个过程可以合并,但在这里讨论的情况下,只是两个模板相互竞争。我不明白基于转换序列的排名(用于函数重载而不是模板重载)如何适用于这里。 - jogojapan
显示剩余2条评论
3个回答

9
这是关键:从大括号初始化列表({expr...})中推断类型对于模板参数不起作用,只适用于auto。 对于模板参数,您会得到一个推断失败,并将该重载从考虑中删除。 这导致了第一和第三个输出。
引用块: 这甚至无论是按值还是按const&传递initializer_list都没有影响。 foo: 对于任何X,取XX&参数的两个重载对于lvalue参数是模棱两可的 - 两者同样可行(对于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 第三个子项)。


遗憾的是,标准似乎没有规定如果特化标准和更可行的标准在使用哪个函数上相互矛盾时会发生什么... - PierreBdR
1
@PierreBdR:是的,用于确定最佳可行函数的情况按它们出现的顺序进行查看。 - Xeo

4
如果Scott真的说他错了,那么问题可能在于他所教授的误导性“通用引用”思维模型。所谓的“通用引用”确实很贪婪,可能会在你不想要或不期望的情况下匹配,但这并不意味着它们总是最佳匹配。非模板重载可以是精确匹配,并且将优先于“通用引用”,例如,这将选择非模板。
bool f(int) { return true; }
template<typename T> void f(T&&) { }
bool b = f(0);

模板重载可以比“通用引用”更加专业化,因此会被选择来进行重载解析。例如:

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&)更专业化,将优先选择lvalues。

在您的两个情况中,initializer_list重载不仅更加专业化,而且类似于{1,2,3}的花括号初始化列表永远无法通过模板参数推导来推导。

您的结果解释如下:

foo( {1,2,3} );

您无法从花括号初始化列表中推导出模板参数,因此 foo(T&&) 推导失败,foo(initializer_list<int>) 是唯一可行的函数。

foo( il );

foo(initializer_list<T>)foo(T&&)更为特化,因此会被重载决议选择。

goo( {1,2,3} );

您无法从初始化列表中推断模板参数,因此goo(initializer_list<int>)是唯一可行的函数。
goo( il );

il 是一个非 const 左值,goo(T&&) 可以被调用,并将 T 推导为 initializer_list<int>&&,因此它的签名是 goo(initializer_list<int>&),这比 goo(initializer_list<int> const&) 更匹配,因为将非 const 的 il 绑定到 const 引用是一个比绑定到非 const 引用更差的转换序列。

上面的评论之一引用了 Scott 的幻灯片,称:“没有意义:URefs 处理所有内容。” 这是正确的,这也正是你可能想要重载的原因!你可能需要一些特定类型的更具体的函数,以及适用于其他所有情况的通用引用函数。你还可以使用 SFINAE 来约束通用引用函数,以防止它处理某些类型,从而使其他重载可以处理它们。

在标准库中,std::async 是一个接受通用引用的重载函数。其中一个重载处理第一个参数为 std::launch 类型的情况,另一个重载处理其他所有情况。SFINAE 防止“其他所有情况”重载贪婪地匹配将 std::launch 作为第一个参数传递的调用。


0

首先,对于foo的反应是有道理的。 initializer_list<T>匹配两个调用并且比较特殊,因此应该这样调用。

对于goo,这与完美转发是同步的。调用goo(il)时,有goo(T&&)(其中T = initializer_list<T>&)和常量引用版本之间的选择。我想调用具有非const引用的版本优先于具有const引用的更专业版本。话虽如此,就标准而言,我不确定这是否为明确定义的情况。

编辑:

请注意,如果没有模板,则可以通过标准的第13.3.3.2段(等级隐式转换序列)解决此问题。问题在于,在模板函数的部分排序将指示调用第二个(更专业)的goo(initializer_list<T> const&),但是隐式转换序列的排名将指示要调用goo(T&&)。所以我想这是一个不明确的情况。


@DyP:这里没有非模板函数。 - Xeo
@Xeo 哦,我太累了... 是的。 - dyp
1
@PierreBdR:在部分排序之前,隐式转换序列会被处理,这使得第一个重载函数更具可行性。 - Xeo
@Xeo 我想你把它做对了。两种转换顺序都是“完全匹配”的级别,但是[over.ics.rank]/3明确偏向于没有资格调整的转换。[over.match.best]/1正如你所说的那样,隐式转换序列在部分排序之前被用来确定最佳重载。 - dyp
1
@Xeo 我会选择“鉴于这些定义,如果对于所有参数i,ICSi(F1)不是比ICSi(F2)更差的转换序列,那么一个可行的函数F1被定义为比另一个可行的函数F2更好的函数。” ;) - dyp
显示剩余2条评论

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