初始化列表中的隐式转换失败

53

考虑以下代码片段:

#include <unordered_map>

void foo(const std::unordered_map<int,int> &) {}

int main()
{
        foo({});
}

使用GCC 4.9.2会失败,并显示以下错误信息:

map2.cpp:7:19: error: converting to ‘const std::unordered_map<int, int>’ from initializer list would use explicit constructor ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::unordered_map(std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::size_type, const hasher&, const key_equal&, const allocator_type&) [with _Key = int; _Tp = int; _Hash = std::hash<int>; _Pred = std::equal_to<int>; _Alloc = std::allocator<std::pair<const int, int> >; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::size_type = long unsigned int; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::hasher = std::hash<int>; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::key_equal = std::equal_to<int>; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::allocator_type = std::allocator<std::pair<const int, int> >]’

与其他编译器/库实现的测试:

  • GCC < 4.9接受此代码而不抱怨,
  • 带有libstdc++的Clang 3.5会失败并显示类似的消息,
  • 带有libc++的Clang 3.5将接受此代码,
  • ICC 15.something 接受此代码(不确定它使用的是哪个标准库)。

还有几个令人困惑的点:

  • std::map替换std::unordered_map会使错误消失,
  • foo({{}})代替foo({})也会使错误消失。

同样,在所有情况下,将{}替换为非空初始化列表都能按预期工作。

所以我的主要问题是:

  • 谁在这里正确?上面的代码格式良好吗?
  • 双大括号语法 foo({{}}) 到底是如何使错误消失的?

编辑修正了一些拼写错误。

2个回答

47
你的代码使用了一个带有花括号初始化列表的间接初始化语法,这被称为复制列表初始化
为此情况选择最佳可行构造函数的重载解析过程在C++标准的下一节中描述:
§ 13.3.1.7 列表初始化的初始化[over.match.list] 当非聚合类类型 T 的对象进行列表初始化 (8.5.4) 时,重载决议会在两个阶段选择构造函数:
- 首先,候选函数是类 T 的初始化器列表构造函数 (8.5.4),参数列表由初始化器列表作为单个参数组成。 - 如果没有可行的初始化器列表构造函数,则再次执行重载决议,其中候选函数是类 T 的所有构造函数,参数列表由初始化器列表的元素组成。
如果初始化器列表没有元素,并且 T 有一个默认构造函数,则省略第一阶段。在复制列表初始化中,如果选择了显式构造函数,则初始化是不合法的。[注意:这与其他情况(13.3.1.3、13.3.1.4)不同,在这些情况下,只有转换构造函数被考虑用于复制初始化。该限制仅适用于此初始化是重载决议最终结果的一部分的情况。—注]。
根据这个,通常比起其他构造函数,初始化列表构造函数(只需一个匹配类型为std::initializer_list<T>的构造函数参数)更受青睐,但是如果有默认构造函数可用,并且用于列表初始化的花括号初始化列表为空,则不适用。
在此需要注意的是,由于LWG问题2193,标准库容器的构造函数集合在C++11和C++14之间发生了变化。对于std::unordered_map,我们关心以下区别:
C++11:
explicit unordered_map(size_type n = /* impl-defined */,
                     const hasher& hf = hasher(),
                     const key_equal& eql = key_equal(),
                     const allocator_type& alloc = allocator_type());

unordered_map(initializer_list<value_type> il,
            size_type n = /* impl-defined */,
            const hasher& hf = hasher(),
            const key_equal& eql = key_equal(),
            const allocator_type& alloc = allocator_type());

C++14:

unordered_map();

explicit unordered_map(size_type n,
                     const hasher& hf = hasher(),
                     const key_equal& eql = key_equal(),
                     const allocator_type& alloc = allocator_type());

unordered_map(initializer_list<value_type> il,
            size_type n = /* impl-defined */,
            const hasher& hf = hasher(),
            const key_equal& eql = key_equal(),
            const allocator_type& alloc = allocator_type());

换句话说,根据语言标准(C++11/C++14),存在不同的默认构造函数(可以无需参数调用的构造函数),并且在C++14中,关键是默认构造函数现在变为非显式(non-explicit)。
这个改变是为了让人们能够这样说:
std::unordered_map<int,int> m = {};

或者:

std::unordered_map<int,int> foo()
{
    return {};
}

这两种方式在语义上等同于您的代码(将{}作为函数调用的参数来初始化std::unordered_map<int,int>)。

也就是说,对于符合C++11标准的库,错误是预期的,因为所选的(默认)构造函数是explicit,因此代码是不合法的

explicit unordered_map(size_type n = /* impl-defined */,
                     const hasher& hf = hasher(),
                     const key_equal& eql = key_equal(),
                     const allocator_type& alloc = allocator_type());

如果一个C++14兼容的库,出现错误是不被期望的,因为所选择的(默认)构造函数是explicit的,并且代码是合法的
unordered_map();

因此,您遇到的不同行为完全与使用不同编译器/编译器选项的libstdc++和libc++版本有关。
我怀疑这只是因为您使用的libstdc++版本中的std :: map已经升级到C ++ 14。将std :: unordered_map替换为std :: map就可以解决问题。

foo({})替换为foo({{}})也使错误消失了。 为什么?

因为现在这是一个 复制列表初始化,使用一个非空的 花括号初始化列表(即,它里面有一个元素,使用空的花括号初始化列表 {}进行初始化),所以第一阶段 §13.3.1.7 [over.match.list]/p1 中的规则(之前引用过)优先选择初始化程序列表构造函数而不是其他构造函数。该构造函数不是explicit的,因此调用是符合语法的


在所有情况下,将{}替换为非空初始化列表都能按预期工作。为什么?
与上面相同,重载决议最终会结束于§13.3.1.7 [over.match.list]/p1的第一阶段。

6

参考[dcl.init.list]/3,对于引用的列表初始化定义如下:

否则,如果T是一个引用类型,则以被T引用的类型的prvalue临时变量为参数,进行复制列表初始化或直接列表初始化,具体取决于引用的初始化方式,并将引用绑定到该临时变量。

因此,您的代码失败是因为

std::unordered_map<int,int> m = {};

失败。对于这种情况,通过[dcl.init.list] / 3中的以下内容来覆盖列表初始化:

否则,如果初始化程序列表没有元素并且T是具有默认构造函数的类类型,则对象将进行值初始化。

因此,将调用对象的默认构造函数1
现在到关键部分: 在C++11中,unordered_map具有此默认构造函数2

explicit unordered_map(size_type n = /* some value */ ,
                       const hasher& hf = hasher(),
                       const key_equal& eql = key_equal(),
                       const allocator_type& a = allocator_type());

显然,通过复制列表初始化调用此explicit构造函数是不合法的,[over.match.list]:

在复制列表初始化中,如果选择了一个explicit构造函数,则该初始化是不合法的。

自C++14以来,unordered_map声明了一个非显式的默认构造函数:

unordered_map();

因此,C++14标准库实现应该可以无需问题地编译这段代码。libc++可能已经更新,但是libstdc++则落后了。


1) [dcl.init]/7:

对于类型为T的对象进行值初始化意味着:
— 如果T是一个有用户提供的构造函数(12.1节)的(可能是cv限定符的)类类型(第9节),则调用T的默认构造函数 […];

2) [class.ctor]/4:

X的默认构造函数是一个可以不带参数调用的类X的构造函数。


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