【问题标题】:Why scalar in braces are not interpeted as initializer_list为什么大括号中的标量不被解释为 initializer_list
【发布时间】:2015-09-21 13:03:35
【问题描述】:

考虑以下代码sn-p:

#include <iostream>
#include <initializer_list>

struct C
{
    C(std::initializer_list<int>) { std::cout << "list\n"; }
    C(std::initializer_list<int>, std::initializer_list<int>) { std::cout << "twice-list\n"; }
};

int main()
{
    C c1 { {1,2}, {3} }; // twice-list ctor
    C c2 { {1}, {2} }; // why not twice-list ?
    return 0;
}

Live 演示。

为什么 c2 变量的大括号中的标量值不被解释为单独的 std::initializer_list?

【问题讨论】:

    标签: c++ language-lawyer initializer-list


    【解决方案1】:

    首先,非常重要的一点:您有两种不同的构造函数。特别是第一个 C(std::initializer_list&lt;int&gt;),称为 initializer-list 构造函数。第二个只是一个普通的用户定义的构造函数。

    [dcl.init.list]/p2

    一个构造函数是一个 initializer-list 构造函数,如果它的第一个参数是 std::initializer_list&lt;E&gt; 类型或对某些类型 E 的可能有 cv 限定的 std::initializer_list&lt;E&gt; 的引用,并且要么没有其他参数或所有其他参数都有默认参数 (8.3.6)。

    在包含一个或多个initializer-clauses 的列表初始化中,initializer-list 构造函数在任何其他构造函数之前被考虑。也就是说,初始化列表构造函数最初是重载决议期间的唯一候选者。

    [over.match.list]/p1

    当非聚合类类型T 的对象被列表初始化,使得8.5.4 指定根据本节中的规则执行重载解析时,重载解析分两个阶段选择构造函数:

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

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

    所以对于c1c2 的两个声明,候选集仅包含C(std::initializer_list&lt;int&gt;) 构造函数。

    在选择构造函数后,对参数求值以查看是否存在将它们转换为参数类型的隐式转换序列。这将我们带到了初始化列表转换的规则:

    [over.ics.list]/p4(强调我的):

    否则,如果参数类型是std::initializer_list&lt;X&gt;并且初始化器列表的所有元素都可以隐式转换为X,则隐式转换序列是转换 列表的元素到X,或者如果初始化列表没有元素,则进行身份转换。

    这意味着如果初始化列表的每个元素都可以转换为int,则存在转换。

    现在让我们关注c1:对于初始化器列表{{1, 2}, {3}},初始化器子句{3}可以转换为int([over.ics.list]/p9.1),但是不是{1, 2}(即int i = {1,2} 格式不正确)。这意味着违反了上述报价的条件。由于没有转换,重载决议失败,因为没有其他可行的构造函数,我们被带回到[over.match.list]/p1的第二阶段:

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

    请注意最后措辞的变化。第二阶段的参数列表不再是单个初始化列表,而是声明中使用的括号初始化列表的参数。这意味着我们可以根据初始化列表单独而不是同时评估隐式转换。

    在初始化器列表{1, 2}中,两个初始化器子句都可以转换为int,因此整个初始化器子句都可以转换为initializer_list&lt;int&gt;{3}也是如此。然后通过选择第二个构造函数来解决重载解决方案。

    现在让我们关注c2,现在应该很容易了。首先计算初始化列表构造函数,并且使用{ {1}, {2} } 肯定存在从{1}{2}int 的转换,因此选择了第一个构造函数。

    【讨论】:

    • 那么,为什么我将第二个构造函数更改为C(std::initializer_list&lt;double&gt;, std::initializer_list&lt;int&gt;)c2 声明为C c2 { {1.0}, {2} }; 我会得到编译器错误?根据您的回答,必须为此选择第二个构造函数。还是我得出了错误的结论? live demo
    • @alexolut [over.isc.list]/p4: "[...] 和初始化器列表的所有元素都可以隐式转换为X,"double 可以是隐式转换为int,因此该子句成立。进一步阅读:“隐式转换序列是将列表元素转换为X所需的最差转换”。这个隐式转换序列是窄化转换序列。所以并不是没有找到可行的初始化列表构造函数(进入阶段 2 意味着我们还没有找到),而是转换本身导致程序格式错误。
    • 即找到了可行的构造函数,但参数转换本身不可行(格式错误)。在这种情况下,我们还没有达到第二阶段。 - 我理解正确吗?
    • @alexolut 是的,没错。另请注意:“如果需要缩小转换 [..] 来转换任何参数,则程序格式错误。”
    【解决方案2】:

    C c2 { {1}, {2} };

    这一行没有传入std::initializer_list&lt;int&gt; 的两个参数,而是传入了一个std::initializer_list&lt;std::initializer_list&lt;int&gt; &gt;。一个解决方案是像这样实例化c2

    C c2({1}, {2});

    【讨论】:

    • 问题是“为什么”。
    • 我明白了。让我快速获取参考并进行编辑。
    • 基本上,“为什么”的答案是因为 std::initializer_list 出现在构造函数中时具有特殊的优先级。如果你有一个带有 std::initializer_list 的构造函数并且你用大括号初始化构造对象,编译器会尽力转换你输入的内容以适应 std::initializer_list 构造函数,即使它不是最合适的
    猜你喜欢
    • 1970-01-01
    • 2019-08-28
    • 1970-01-01
    • 2019-06-11
    • 2020-08-17
    • 2016-07-28
    • 2010-12-20
    • 2020-01-04
    • 1970-01-01
    相关资源
    最近更新 更多