技术: C++11 一致性初始化探究

C++11之前, 初始化方式,乱七八糟,不同编译器的对待方式也不太一样。

新容器 initializer_list 容器,统一了初始化方式。

旧方式

如下:
单个对象:

1
2
std::string s("hello");
std::string s="hello";

数组结构体(POD):

1
2
int arr[4]={0,1,2,3};  或者{0}
struct tm today={0};

类数据成员: 初始化列表.

新方式

C++11统一用大括号进行处理了(可能看上去初始化成员数据时候比较奇怪)
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class C  
{
private:
int a = 10; //C++允许你直接在这里进行初始化, 有点儿像Java
int b;
public:
C(int i, int j);
};


C c {0,0}; //C++11 only. 相当于 C c(0,0);
int* a = new int[3] { 1, 2, 0 }; /C++11 only

class X
{
private:
int a[4];
public:
X(): a{1,2,3,4} {//具体的实现逻辑} //C++11, 初始化数组成员
};

其中对象的初始化也用大括号C c {0,0}; //C c(0,0);感觉稍稍也有点不适应.

其实我觉得, 最实用的就是, 容器&数组的初始化, 太棒了:

1
2
3
4
5
6
7
8
9
10
//array
int values[]{1,2};

// C++11 container initializer
vector<string> vs { "first", "second", "third"};

map<string, string> singers
{ {"Lady Gaga", "+1 (212) 555-7890"},
{"Beyonce Knowles", "+1 (212) 555-0987"}
};

实现剖析

内部实现

所有的简单机制背后, 都有一个稍稍复杂的机制, 或者对你透明(隐藏)的工程细节.

其实借助了 统一的初始化列表, 即initializer_list<T>, 在大括号内的所有内容, 都会被装入initializer_list然后用于给不同的变量&对象, 进行初始化.

initializer_list背后关联了一个array容器, 初始化时, array内部的元素会被编译器逐一传递给相关的变量&对象&容器, 如果初始化函数, 比如构造器的参数本身就是一个initializer_list, 那么你可以按照要求传递即可(此时不用从里面拆分元素了). 如果没有这样构造器来初始化变量&对象, 那么它会把大括号形成的initializer_list进行拆解, 然后去匹配相应的调用构造器.

如果还不满足, 比如构造器只有接受两个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
2
int j{}; //默认赋值为0, 因为是int型
int* q{}; //默认赋值为nullptr

安全检查

以往的初始化, 有一些处理会难以察觉, 比如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
2
3
4
5
6
void print(std::initializer_list<int> vals)
{
for(auto p=vals.begin(); p!=vals.end(); ++p) {
std::cout << *p << std::endl;
}
}

调用的时候:

1
print({1, 2, 3})

只要你加上了大括号, 调用重载的时候, 都是优先找参数为std::initializer_list<int>的构造器.

拷贝问题? 不要担心数据成员array的浅拷贝问题, 上面也解释了.

但是劝你不要这么用, 因为容器保存的元素的引用, 而不是拷贝.

1
2
3
4
5
std::initializer_list<int> func(void)  
{
auto a = 2, b = 3;
return{ a, b };
}

另外, 它只能被整体的初始化和赋值,遍历只能通过begin和end迭代器来,遍历取得的数据是只读的,是不能对单个进行修改的

算法适配

有一些算法, 随着initializer_list容器的引入, 也做了适配, 比如std::max, std::min原来只接受两个参数, 现在你直接把要比较的参数用{}中括号包起来起来就能用了, 因为这些算法, 有新的重载版本, 它们接收initializer_list作为参数.

注意事项

总结一句话, initilizer-list是依赖于构造器的(基本类型的变量除外)

如果你想不提供构造器, 使用编译器生成的默认的完成使用initializer-list的任务, 最好的情况是类是聚合体,

  • 无用户自定义构造函数
  • 无私有或者受保护的非静态数据成员
  • 无基类
  • 无虚函数
  • 无{}和=直接初始化的非静态数据成员

例如, 没有定义构造器的情况, 可以使用默认的进行, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

using namespace std;

struct Foo
{
int x;
int y;
};

int main()
{
Foo foo{ 123, 321 };
cout << foo.x << " " << foo.y;

return 0;
}

但是一旦用户制定了构造器(或者有了非私有成员, 比如受保护的, 或者有静态成员, 或者有虚函数, 或者有基类, 或者在类的默认初始化成员了), 就要负责成员的初始化问题,
否则编译器用initializaer-list找不到初始化方式(默认构造器已经失效了).

下面的结果就非常奇怪:(提供了构造器, 却不手动进行成员的初始化处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

using namespace std;

struct Foo
{
int x;
int y;
Foo(int, int){ cout << "Foo construction"; }
};

int main()
{
Foo foo{ 123, 321 }; //此时初始化列表失效, 对象的成员根本没有初始化
cout << foo.x << " " << foo.y;

return 0;
}

好了, 源码也说了, 先这样, 其他请参考, 《Effective Modern C++》, 《深入应用C++11 代码优化与工程机应用》

C++11引入了很多机制, 为了这些机制又引入了更多的机制; 好在编译器做的多, 我们稍微少做一点儿

文章目录
  1. 1. 旧方式
  2. 2. 新方式
  3. 3. 实现剖析
    1. 3.1. 内部实现
    2. 3.2. 内部array
  4. 4. initializer_list
    1. 4.1. 默认赋值
    2. 4.2. 安全检查
    3. 4.3. 单独使用
    4. 4.4. 算法适配
  5. 5. 注意事项
|