C++11之前, 初始化方式,乱七八糟,不同编译器的对待方式也不太一样。
新容器 initializer_list
容器,统一了初始化方式。
旧方式
如下:
单个对象:
1 | std::string s("hello"); |
数组结构体(POD):
1 | int arr[4]={0,1,2,3}; 或者{0} |
类数据成员: 初始化列表.
新方式
C++11统一用大括号进行处理了(可能看上去初始化成员数据时候比较奇怪)
例如:
1 | class C |
其中对象的初始化也用大括号C c {0,0}; //C c(0,0);
感觉稍稍也有点不适应.
其实我觉得, 最实用的就是, 容器&数组的初始化, 太棒了:
1 | //array |
实现剖析
内部实现
所有的简单机制背后, 都有一个稍稍复杂的机制, 或者对你透明(隐藏)的工程细节.
其实借助了 统一的初始化列表, 即initializer_list<T>
, 在大括号内的所有内容, 都会被装入initializer_list
initializer_list背后关联了一个array
如果还不满足, 比如构造器只有接受两个int的版本, 你在大括号里写了3个int值, 所以拆解也匹配不了, 此时你需要自己再重载相应构造器来满足要求.
具体的说, vector<string> vs { "first", "second", "third"};
之所以成功, 是因为编译器可以找到vector<string>
类中, 接收initializer_list<string>
的构造器或者拆解之后, 找到了接收三个参数的构造器.
(initializer_list<string>
背后关联了array<string, 6>
这个容器, 你看这个类的数据成员又一个_M_array
)
如果是对象, 比如C c{1,2};
(原来的写法, C c(1,2);
)之所以能够编译通过, 也是因为C类有接收initializer_list<int>
(array<int,2>
)为参数的构造器. 如果没有这样的构造器, 要么你手动提供一个这样的重载, 要么编译器报错.
看到了吧, 所有的简单机制背后, 都有一个稍稍复杂的机制, 或者对你透明(隐藏)的工程细节.
其实标准库做了很多改进工作.
内部array
这部分你要看initializer_list这个类的私有构造器.
其实从大括号的参数, 到构件完成一个initializer对象, 编译器调用的是私有构造器, 这个时候才会在内部关联一个array对象. 或者说编译器再调用私有构造器之前就准备好了一个array用来盛放具体的初始化参数, 然后私有构造器接收的是array的头迭代器以及array的长度.
注意, initializer_list只是保存了array的引用, 以及array的长度, 没有傻傻的把它拷贝进来. (即浅拷贝)
那么问题来了:
浅拷贝问题? (浅拷贝不安全)不用担心
除非是编译器可以调用的私有构造器, 只接受array的指针和长度; 你手动调用的构造器不存在浅拷贝的问题(如果你要单独使用 initializer_list 这个容器的话).
私有构造器, 只有编译器有权利调用
btw, 那么array是啥?
通俗的理解, array就是C++的数组, 和[]
中括号指定的没有本质区别, 都是顺序存储, 但是比C数组好的地方在于, 它和容器接口统一
, begin, end, size等函数都是有的, 调用方式上完成了统一, 准确来说, 是迭代器和容器统一了, 当然和容器保持一致的好处是, 你可以用标准库的算法
了.
下面专门说收initializer
initializer_list
使用initializer有很多好处, 默认赋值, 安全检查等.
默认赋值
例如:
1 | int j{}; //默认赋值为0, 因为是int型 |
安全检查
以往的初始化, 有一些处理会难以察觉, 比如char
类型赋值9999
, 溢出了, 当然有的会自动截断, 例如: int x1 = 5.3
, 但是使用initializer_list时, 情况就不一样, 它会让编译器报错(或者警告), 例如int x1{5.3}
或 int x1 = {5.3}
, 因为从double隐式转换到int, 会进行截断(narrowing cast), 这是不安全的.
单独使用
直接使用std::initializer_list<>
也可以. 并且他也能用于实现变参模板(毕竟参数个数不限定), 但是比variadic templates弱的是, 所有参数的类型要一样.
比如下面的代码:
1 | void print(std::initializer_list<int> vals) |
调用的时候:
1 | print({1, 2, 3}) |
只要你加上了大括号, 调用重载的时候, 都是优先找参数为std::initializer_list<int>
的构造器.
拷贝问题? 不要担心数据成员array的浅拷贝问题, 上面也解释了.
但是劝你不要这么用, 因为容器保存的元素的引用, 而不是拷贝.
1 | std::initializer_list<int> func(void) |
另外, 它只能被整体的初始化和赋值,遍历只能通过begin和end迭代器来,遍历取得的数据是只读的
,是不能对单个进行修改的
算法适配
有一些算法, 随着initializer_list
容器的引入, 也做了适配, 比如std::max
, std::min
原来只接受两个参数, 现在你直接把要比较的参数用{}
中括号包起来起来就能用了, 因为这些算法, 有新的重载版本, 它们接收initializer_list作为参数.
注意事项
总结一句话, initilizer-list是依赖于构造器的(基本类型的变量除外)
如果你想不提供构造器, 使用编译器生成的默认的完成使用initializer-list的任务, 最好的情况是类是聚合体
,
- 无用户自定义构造函数
- 无私有或者受保护的非静态数据成员
- 无基类
- 无虚函数
- 无{}和=直接初始化的非静态数据成员
例如, 没有定义构造器的情况, 可以使用默认的进行, 如下
1 |
|
但是一旦用户制定了构造器(或者有了非私有成员, 比如受保护的, 或者有静态成员, 或者有虚函数, 或者有基类, 或者在类的默认初始化成员了), 就要负责成员的初始化问题,
否则编译器用initializaer-list找不到初始化方式(默认构造器已经失效了).
下面的结果就非常奇怪:(提供了构造器, 却不手动进行成员的初始化处理)
1 |
|
好了, 源码也说了, 先这样, 其他请参考, 《Effective Modern C++》
, 《深入应用C++11 代码优化与工程机应用》
C++11引入了很多机制, 为了这些机制又引入了更多的机制; 好在编译器做的多, 我们稍微少做一点儿