直接列表初始化和复制列表初始化的区别

26

我想知道在C++11及以后版本中以下两种std::vector初始化方式是否有差异。

std::vector<int> v1 {1, 2, 3, 4, 5};
std::vector<int> v2 = {1, 2, 3, 4, 5};

这是一个完整的代码示例,可以正常工作。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v1 {1, 2, 3, 4, 5};
    std::vector<int> v2 = {1, 2, 3, 4, 5};

    std::cout << v1.size() << '\n';
    std::cout << v2.size() << '\n';
}

我看到两种初始化方式都会导致相同的结果。

http://en.cppreference.com/w/cpp/container/vector 的例子中使用了第二种方式,这让我想知道这种初始化方式是否有任何优点。

总的来说,我想知道其中一种初始化方式是否具有特定的技术优势,或者其中一种初始化方式是否被视为最佳实践而另一种不是,如果是的话,为什么。

特别是,我担心的是复制列表初始化是否由于临时对象和复制而具有额外的开销?


1
第一个称为直接列表初始化,第二个称为复制列表初始化。对于vector来说,这两个东西的行为完全相同。 - M.M
@M.M 在使用 copy-list-initialization 时,是否存在与 direct-list-initialization 不同的行为?在 copy-list-initialization 中是否涉及任何复制操作,从而导致其速度变慢? - Lone Learner
@Frank 如果这些更改可以简洁地解释,那么我们能否得到一个回答,解释C++11、C++14和C++17中直接列表初始化复制列表初始化之间的区别?如果答案过于宽泛和冗长,那么我将不得不将其分成三个问题,我不确定这是否合适。 - Lone Learner
值得注意的是,在上面的例子中,不同的编译器编译出相同的代码。 - andreee
@Frank:“这取决于涉及的标准版本(C++14中的一些相关更改在这里发挥作用)”。有哪些更改?C++14没有改变复制列表或直接列表初始化的行为。它改变了列表初始化的行为,但这些更改同样适用于两者。 - Nicol Bolas
显示剩余2条评论
2个回答

29
列表初始化通常被称为“统一初始化”,因为它的意义和行为是相同的,无论你如何调用它。当然,C++就是C++,有时候“意图”并不总是发生。基本上,直接列表初始化和复制列表初始化的行为有三个主要差异。第一个是你最常遇到的:如果列表初始化将调用标记为“explicit”的构造函数,则在列表初始化形式为复制列表初始化时会出现编译错误。这种行为差异在[over.match.list]/1中定义。在复制列表初始化中,如果选择了显式构造函数,则初始化无效。这是重载分辨率的一个功能。第二个主要差异(C++17新特性)是,在具有固定底层大小的枚举类型中,您可以使用底层类型的值对其执行直接列表初始化。但是,您不能从此类值执行复制列表初始化。因此,enumeration e{value};有效,但enumeration e = {value};无效。第三个主要差异(也是C++17的新特性)与大括号初始化列表在auto推导中的行为有关。通常情况下,auto的行为与模板参数推导并没有太大区别。但与模板参数推导不同,auto可以从大括号初始化列表初始化。
如果您使用单个表达式在列表中进行直接列表初始化来初始化auto变量,则编译器将推断该变量的类型为该表达式的类型:
auto x{50.0f}; //x is a `float`.

听起来很合理。但如果使用复制列表初始化做完全相同的事情,它将始终被推导为一个initializer_list,其中T是初始化程序的类型:
auto x = {50.0f}; //x is an `initializer_list<float>`

非常、非常一致。 ;)
幸运的是,如果在花括号初始化列表中使用多个初始化器,则对于auto推断的变量,直接列表初始化将始终导致编译错误,而复制列表初始化只会给出更长的initializer_list。因此,auto-deduced直接列表初始化永远不会提供initializer_list,而auto-deduced复制列表初始化总是会提供。
有一些微小的差异,很少影响初始化的预期行为。这些是单值列表初始化将根据列表初始化形式适当使用复制或直接(非列表)初始化的情况。这些情况包括:
1. 从与正在初始化的聚合体相同类型的单个值初始化聚合体。这将绕过聚合初始化。 2. 从单个值初始化非类、非枚举类型。 3. 初始化引用。
这些情况并不经常发生,它们基本上从不真正改变代码的含义。非类类型没有显式构造函数,因此复制和直接初始化之间的区别大多是学术性的。引用也是如此。而聚合体的情况实际上只是从给定值执行复制/移动的情况。

1
这是一个非常好的答案。为了完整起见,您是否愿意添加评论中所说的内容,即std::vector v {0, 1};是直接列表初始化,而std::vector v = {0, 1};是复制列表初始化? - YSC
1
@YSC:原帖作者知道哪个是哪个。问题是它们有什么不同之处。 - Nicol Bolas
1
这不是给楼主的,而是给未来的读者:如果有人不知道哪个是哪个进入了这个问答,我担心他们无法从您的好答案中获得最大的收益,因为他们会错过关键信息。 - YSC
1
SO是否禁止在评论中讨论关于良好编码风格的看法? - Lone Learner
3
你错过了http://eel.is/c++draft/dcl.init.list#3.8,还有一些“(通过复制初始化进行复制列表初始化,或通过直接初始化进行直接列表初始化)”的情况。 - T.C.
显示剩余2条评论

7

我普遍更喜欢使用复制列表初始化(copy-list initialization),尤其是在处理std::vector时,因为直接列表初始化(direct-list initialization)和使用std::vector实际构造函数时只有一个或两个元素时非常相似。

std::vector<int> x(2);
// is very different from
std::vector<int> x{2};

明确指出我正在为向量分配初始值而不是配置它的值,这样更不容易被误读。


2
我不确定这与问题有什么关联。 - Barry
@Barry "或者如果一个初始化被认为是最佳实践,而另一个不是,如果是这样,为什么。" 代码可读性/可审查性问题绝对符合问题的这一部分。 - user4442671
1
其中,“one”和“the other”是直接列表初始化和复制列表初始化。 - Barry
@Barry 我刚意识到问题的标题自我回答后已经更改,所以措辞现在看起来确实不合适。我稍微修改了一下,以更好地匹配新问题。 - user4442671

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