4

C++ 的花括号初始化

 3 years ago
source link: https://zhiqiang.org/coding/list-initializer-in-cpp.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

花括号初始化是C++11引入的一种初始化方法。

1. 花括号初始化的语法

1.1. 直接初始化

T object { arg1, arg2, ... };   
T { arg1, arg2, ... }; 
new T { arg1, arg2, ... }
class Class { T member { arg1, arg2, ... }; };   
Class::Class() : member{arg1, arg2, ...} {...  }

1.2. 复制初始化

T object = {arg1, arg2, ...};
function( { arg1, arg2, ... } ) ;
return { arg1, arg2, ... } ;  
object[ { arg1, arg2, ... } ] ;
object = { arg1, arg2, ... } ;  
U( { arg1, arg2, ... } )    
class Class { T member = { arg1, arg2, ... }; };

使用花括号初始化时,编译必须加上--std=c++11选项。

2. 花括号初始化的规则

C++的复杂之处在这里体现的淋漓尽致,当编译器遇到花括号时,规则见

http://en.cppreference.com/w/cpp/language/list_initialization

规则很长。除去那些简单的情况,约有三种情况,依次优先:

  • 直接初始化(逐个赋值)(此时一个重要特点是没有定义初始化函数)。
  • 提供列表初始化的初始化函数。
  • 参数依次匹配的初始化函数。

2.1. 聚合初始化 aggregate initialization

当类型是一个聚合结构( aggragates )时,花括号将进行聚合初始化。

聚合结构是指一个数组( array )或者一个满足下面条件的结果:

  • 所有非静态的成员都是共有的( public )
  • 没有自定义的初始化函数。
  • 没有虚拟成员函数
  • 继承基于 public

聚合结构可以简单理解为 c 语言里的普通结构。聚合初始化时,结构用花括号里的数据依次填充成员变量。当然,具体的规则要复杂很多,详情可见:

http://en.cppreference.com/w/cpp/language/aggregate_initialization

struct S {
    int x;
    struct Foo {
        int i;
        int j;
        int a[3];
    } b;
};

int main()
{
    S s1 = { 1, { 2, 3, {4, 5, 6} } };
    S s2 = { 1, 2, 3, 4, 5, 6}; // same, but with brace elision
    S s3{1, {2, 3, {4, 5, 6} } }; // same, using direct-list-initialization syntax
}

2.2. 初始化列表初始化 initializer list initialization

这里指 C++编译器遇到花括号列表时,将其作为一个std::initializer_list的对象,当初始化对象存在接受std::initializer_list为参数的构造函数时,编译器将调用该构造函数直接进行初始化。

std::initializer_list的文档:

http://en.cppreference.com/w/cpp/utility/initializer_list

很多std容器如vectormapset等都有接受std::initializer_list的构造函数,因此可以像普通数组一样使用花括号列表初始化。

2.3. 匹配构造函数参数

当上面两种情况不存在时,编译器将依次匹配初始化对象的所有初始化函数,调取其中参数匹配的。如下例:

std::vector<std::string> strs1{100, "abc"}; // it's the same with below
std::vector<std::string> strs2(100, "abc");

还有个特殊的情况,是匹配构造函数参数和上面的初始化列表初始化的的结合。比如下面这种写法:

std::vector<int> x{{1, 2}};

注意它的结果和下面一样,但没有歧义了。因为下面这个容易被误解为调用std::vector<int>(1, 2)的构造函数:

std::vector<int> x{1, 2};

编译器遇到最外层的花括号时,开始匹配构造函数参数,内层的花括号表示参数为初始化列表。

2.4. 复制初始化 copy initialized

上面谈到的三种情况都是直接初始化( direct initialized ),对应的是上面第一部分的直接初始化的语法。而对于第一部分中的复制初始化的语法,将采取复制初始化。

字面上而言,复制初始化将先使用上面三种直接初始化方法之一初始化一个临时变量,然后调用copymove函数将临时对象复制到需初始化的对象上。

但实际操作中,编译器会使用一种叫做copy elision的技术。简而言之就是已知临时变量必然没有用处,那我也不需要建立临时变量,直接用直接初始化方法好了。

因此,在类似std::vector<int> x = {1, 2, 3}这样的语法中,最后生成的二进制代码将和std::vector<int> x{1, 2, 3}一致,它们的效率一模一样。

但复制初始化和直接初始化在编译期还是有区别,区别在于编译器会检查复制构造函数的存在性和可访问性,虽然编译器最后并没有用到它。

比如std::atomic禁用了复制构造函数(一般设置为=delete或者把复制构造函数设置为 private ),下面这种初始化就无法编译:

std::atomic<int> x = 1; // not valid because copy constructor not exist

你必须使用括号或者花括号:

std::atomic<int> x1(1);   // valid
std::atomic<int> x2{1};   // valid, the same as x2(1);

3. 花括号的良好编程习惯

我个人坚持的习惯有两个:

  • 尽量使用复制初始化。即std::vector<int> x = {1, 2, 3}要好于std::vector<int> x{1, 2, 3}。它们两者的效率无差异,但前者的可读性要高于后者。
  • 当调用普通构造函数时,尽量不要使用花括号,而要用小括号代替,以免引起歧义。比如不要用std::vector<std::string> strs{100, "abc"},而要用std::vector<std::string> strs(100, "abc")

Q. E. D.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK