【问题标题】:Implicit conversion failure from initializer list初始化列表中的隐式转换失败
【发布时间】:2015-01-12 21:21:38
【问题描述】:

考虑sn-p:

#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
  • 带有 libstdc++ 的 Clang 3.5 失败并显示类似消息,
  • 带有 libc++ 的 Clang 3.5 接受这一点,
  • ICC 15.something 接受这个(不确定它使用的是哪个标准库)。

还有几个令人困惑的地方:

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

此外,用非空初始化列表替换 {} 在所有情况下都可以正常工作。

所以我的主要问题是:

  • 谁在这里?上面的代码格式正确吗?
  • 带有双花括号 foo({{}}) 的语法究竟是如何使错误消失的?

EDIT 修正了几个错别字。

【问题讨论】:

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


    【解决方案1】:

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

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

    § 13.3.1.7 通过列表初始化 [over.match.list] 进行初始化

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

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

      ——如果没有找到可行的初始化列表构造函数,则再次执行重载决议,其中候选函数是类T的所有构造函数,参数列表由初始化列表的元素组成。

    如果初始化列表没有元素并且T 有默认构造函数,则省略第一阶段。在复制列表初始化中,如果选择了显式构造函数,则初始化格式错误。 [ 注意: 这与其他情况(13.3.1.3、13.3.1.4)不同,在其他情况下,复制初始化只考虑转换构造函数。仅当此初始化是重载决议的最终结果的一部分时,此限制才适用。 ——结束注释].

    据此,initializer-list-constructor(具有与构造函数的std::initializer_list&lt;T&gt; 类型的参数匹配的单个参数的可调用函数)通常优于其他构造函数,但不是如果默认构造函数可用,并且用于list-initializationbraced-init-list为空。 p>

    这里重要的是,由于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&lt;int,int&gt;)。

    也就是说,在符合 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 的库,错误是不期望,因为选择的(默认)构造函数是 notexplicit,并且代码格式正确

    unordered_map();
    

    因此,您遇到的不同行为仅与您使用不同编译器/编译器选项的 libstdc++ 和 libc++ 版本有关。


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

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


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

    因为现在这是 copy-list-initialization {{}} 带有 non-empty 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 的第一阶段结束。

    【讨论】:

      【解决方案2】:

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

      否则,如果T 是引用类型,则该类型的prvalue 临时 T 引用的是复制列表初始化或直接列表初始化, 取决于参考的初始化类型,以及 引用绑定到那个临时的。

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

      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 是 (可能是 cv 限定的)类类型(第 9 条),带有用户提供的 构造函数(12.1),然后调用T的默认构造函数 […];

      2) [class.ctor]/4:

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

      【讨论】:

        猜你喜欢
        • 2016-04-27
        • 1970-01-01
        • 1970-01-01
        • 2020-09-13
        • 1970-01-01
        • 2011-10-12
        • 1970-01-01
        • 1970-01-01
        • 2012-03-25
        相关资源
        最近更新 更多