为啥在使用大括号初始值设定项列表时首选 std::initializer_list 构造函数?

Posted

技术标签:

【中文标题】为啥在使用大括号初始值设定项列表时首选 std::initializer_list 构造函数?【英文标题】:Why is the std::initializer_list constructor preferred when using a braced initializer list?为什么在使用大括号初始值设定项列表时首选 std::initializer_list 构造函数? 【发布时间】:2015-01-24 11:31:23 【问题描述】:

考虑代码

#include <iostream>

class Foo

    int val_;
public:
    Foo(std::initializer_list<Foo> il)
    
        std::cout << "initializer_list ctor" << std::endl;
    
    /* explicit */ Foo(int val): val_(val)
    
        std::cout << "ctor" << std::endl;
    ;
;

int main(int argc, char const *argv[])

    // why is the initializer_list ctor invoked?
    Foo foo 10; 

输出是

ctor
initializer_list ctor

据我了解,10 的值被隐式转换为Foo(第一个ctor 输出),然后初始化构造器启动(第二个initializer_list ctor 输出)。我的问题是为什么会这样?标准构造函数Foo(int)不是更好的匹配吗?即,我希望这个 sn-p 的输出只是 ctor

PS:如果我将构造函数Foo(int) 标记为explicit,那么Foo(int) 是唯一调用的构造函数,因为整数10 现在不能隐式转换为Foo

【问题讨论】:

我知道它胜过常规构造函数,但不知道即使常规构造函数更匹配,它也胜过。是的,这样似乎有点奇怪。有什么特别的原因吗?这样就可以隐藏复制构造函数了(其实我的代码会隐藏复制构造函数,不是吗?) Scott Meyers 的新书《Effective Modern C++》中有一个关于各种初始化样式的非常好的条目:“条目 7:在创建对象时区分 () 和 ”。它并没有提供太多行为的基本原理,但确实详细介绍了一些可能会让您感到惊讶的边缘情况。 @MichaelBurr 谢谢,我还在等实体版 :) 我知道这不相关,但谁能告诉我我应该在构造函数中按值还是按 const 引用来设置 initializer_list ?那是什么原因呢? 【参考方案1】:

关于初始化列表的n2100 提案非常详细地说明了使序列构造函数(他们称之为采用std::initializer_lists 的构造函数)优先于常规构造函数的决定。详细讨论见附录 B。总结的很简洁:

11.4 结论

那么,我们如何在剩下的两种选择(“歧义”和“序列构造函数优先”之间做出选择? 超过普通的构造函数)?我们的提议给出了序列构造函数 优先,因为

在所有构造函数中寻找歧义导致过多的“误报”;也就是说,明显不相关的冲突 构造函数。请参阅下面的示例。 消歧本身很容易出错(也很冗长)。请参阅第 11.3 节中的示例。 对同质列表的每个元素使用完全相同的语法很重要 - 应该消除歧义 普通构造函数(没有规则的 论据)。请参见第 11.3 节中的示例。最简单的假例子 positive 是默认构造函数:

最简单的误报示例是默认构造函数:

vector<int> v; 
vector<int> v  ; // potentially ambiguous
void f(vector<int>&); 
// ...
f( ); // potentially ambiguous

可以想到没有初始化的类 成员在语义上与默认初始化不同,但我们 不会使语言复杂化以提供更好的支持 比在语义上更常见的情况 一样。

优先考虑序列构造函数会破坏参数检查 更易于理解的块并提供更好的局部性。

void f(const vector<double>&);
// ...
struct X  X(int); /* ... */ ;
void f(X);
// ...
f(1);     // call f(X); vector’s constructor is explicit
f(1);   // potentially ambiguous: X or vector?
f(1,2); // potentially ambiguous: 1 or 2 elements of vector

在这里,优先考虑序列构造函数消除了 来自 X 的干扰。为 f(1) 选择 X 是问题的一个变体 §3.3 中明确显示。

【讨论】:

【参考方案2】:

§13.3.1.7 [over.match.list]/p1:

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

最初,候选函数是类 T 的初始化列表构造函数 (8.5.4),参数列表包括 初始化器列表作为单个参数。 如果没有找到可行的初始化列表构造函数,则再次执行重载决议,其中候选函数都是 T 类的构造函数和参数列表包括 初始化列表的元素。

如果初始化列表没有元素并且T有一个默认值 构造函数,省略第一阶段。在复制列表初始化中, 如果选择了explicit 构造函数,则初始化为 格式不正确。

只要有一个可行的初始化列表构造函数,当使用列表初始化并且初始化列表至少有一个元素时,它将胜过所有非初始化列表构造函数。

【讨论】:

好的,+1,我认为这很到位!出于某种原因,我认为如果需要转换,init_list ctors 不会胜过,但似乎并非如此。但是,如果是这种情况,那么我的代码似乎隐藏了一个潜在的复制构造函数,不是吗?因为 copy ctor 只会复制 init list ctor 的参数,而后者可以做任何我想做的事情。 @vsoftco 好吧,这条规则仅适用于您使用列表初始化的情况。当您不使用 时,将调用复制 ctor 就好了。隐含的特殊成员函数确实可以“隐藏”。一种常见的情况是模板构造函数采用单个转发引用参数可选地后跟一个包(例如,template&lt;class T&gt; Foo(T&amp;&amp;)),这通常比采用const Foo &amp; 的复制ctor 更好。 @vsoftco,您拥有的代码只是隐藏了一个潜在的副本,因为您拥有Foo(std::initializer_list&lt;Foo&gt;),这对我来说毫无意义。那个构造函数是干什么用的?无论如何,如果您使用非空的花括号初始化列表并且有一个初始化列表构造函数,它将被使用,即使这需要创建一个 Foo 对象数组来将 std::initializer_list 绑定到。 @JonathanWakely 这只是一个玩具示例,因为我试图了解 std::initializer_list 构造函数的工作原理。我没有在现实中使用它:)【参考方案3】:

整个初始化列表是为了像这样启用列表初始化:

std::vector<int> v  0, 1, 2 ;

考虑案例

std::vector<int> v  123 ;

这是有意用一个值为 123 的元素而不是 123 个值为 0 的元素来初始化向量。

要访问其他构造函数,请使用旧语法

Foo foo(10);

【讨论】:

这是有道理的,因为 initialzier_list 和常规 ctor 都将 int 作为参数。令我惊讶的是,即使在 initializer_list ctor 中需要转换,后者仍然是首选。 @seldon 是“旧语法”,是摆脱initializer_list 的唯一方法。我的意思是,这是标准方式吗?

以上是关于为啥在使用大括号初始值设定项列表时首选 std::initializer_list 构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

无法从大括号括起来的初始值设定项列表转换

如何将 std::map::operator= 与初始值设定项列表一起使用

大括号初始值设定项列表中是不是允许显式转换运算符?

为啥 C# 3.0 对象初始值设定项构造函数括号是可选的?

C++ 不能用初始值设定项列表初始化非聚合错误

大括号之谜:C++的列表初始化语法解析