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

时间:2014-11-15 15:59:46

标签: c++ c++11 gcc libstdc++

考虑一下代码段:

#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&lt; 4.9接受此而不抱怨,
  • 使用libstdc ++的Clang 3.5失败并显示类似的消息,
  • 使用libc ++的Clang 3.5接受此,
  • ICC 15.something接受这个(不确定它正在使用哪个标准库)。

还有一些令人困惑的问题:

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

此外,将{}替换为非空初始值设定项列表在所有情况下均可按预期工作。

所以我的主要问题是:

  • 谁在这儿?上面的代码是否格式正确?
  • 双花括号foo({{}})的语法是如何使错误消失的呢?

编辑修复了一些错别字。

2 个答案:

答案 0 :(得分:36)

使用 braced-init-list 代码的间接初始化语法称为 copy-list-initialization

在C ++标准的以下部分中描述了为该情况选择最佳可行构造函数的重载解析过程:

  

§13.3.1.7按列表初始化[over.match.list]

初始化      
      
  1. 当非聚合类类型T的对象被列表初始化(8.5.4)时,重载决策选择构造函数   分两个阶段:

         

    - 最初,候选函数是类T的初始化列表构造函数(8.5.4),参数列表由初始化列表作为单个参数组成。

         

    - 如果找不到可行的初始化列表构造函数,则再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始化列表的元素组成。 / p>

  2.         

    如果初始化列表没有元素且T具有默认构造函数,则省略第一个阶段。在copy-list-initialization中,如果选择了显式构造函数,则初始化是错误的。 [注意:这与其他情况(13.3.1.3,13.3.1.4)不同,其中只考虑转换构造函数进行复制初始化。此限制仅适用于此初始化是重载解析的最终结果的一部分。 - 结束记录]。

根据这个, initializer-list-constructor (一个可以使用与std::initializer_list<T>类型的构造函数的参数匹配的单个参数调用的那个)通常比其他构造函数更受欢迎,但是,如果默认构造函数可用,则用于列表初始化 braced-init-list 为空

重要的是,由于LWG issue 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中的默认构造函数现在变为非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 ++版本相关。


  

std::unordered_map替换std::map会导致错误消失。为什么?

我怀疑这只是因为您使用的libstdc ++版本中的std::map已经针对C ++ 14进行了更新。


  

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

因为现在这是 copy-list-initialization {{}},其中包含非空 braced-init-list (是,它内部有一个元素,用空的 braced-init-list {}初始化,所以第13.3.1.7节的第一阶段的规则[over.match.list] / p1(之前引用)优先使用 initializer-list-constructor 到其他的。该构造函数不是explicit,因此调用格式良好


  

使用非空初始值设定项列表替换{}在所有情况下都按预期工作。为什么?

同上,重载决议以第13.3.1.7节[over.match.list] / p1的第一阶段结束。

答案 1 :(得分:5)

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

  

否则,如果T是引用类型,则为该类型的prvalue临时值   T引用的是copy-list-initialized或direct-list-initialized,   取决于参考的初始化类型,以及   引用绑定到那个临时的。

所以你的代码失败了,因为

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());

显然,通过copy-list-initialization调用这个explicit构造函数是不正确的,[over.match.list]:

  

在copy-list-initialization中,如果选择了explicit构造函数,则初始化是不正确的。

由于C ++ 14 unordered_map声明了一个非显式的默认构造函数:

unordered_map();

所以C ++ 14标准库实现应该没有问题地编译它。据推测,libc ++已经更新,但libstdc ++已经落后了。

<小时/> 1) [dcl.init] / 7:

  

值初始化 T类型的对象意味着:
- 如果T是   具有用户提供的(可能是cv-qualified)类类型(第9条)   构造函数(12.1),然后调用T的默认构造函数   [...];

2) [class.ctor] / 4:

  

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