得之我幸 失之我命

when someone abandons you,it is him that gets loss because he lost someone who truly loves him but you just lost one who doesn’t love you.

Cpp11 的大括号初始化

这是第二个 FB 了,从测试到 cpp 开发的过程,虽然依然 “兼职” 着测试方面的事,但是也开始了自己写 cpp 的路,这不,在写的过程中,发现了一个问题,或者说,发现了类用 {} 初始化参数跟之前的学到的不太一样

搜索一番,原来这是 C++11 的特性

等号初始化,赋值,傻傻分不清

首先,区分一下初始化和赋值,虽然对于内置类型,例如 int,初始化和赋值操作的差别是模糊的;但事实是,初始化不是赋值,初始化的含义是创建变量赋予其一个初始值,而赋值的含义是把当前值擦除,而以一个新值来替代。

对象初始化可以分为默认初始化、直接初始化、拷贝初始化以及值初始化

  1. 默认初始化

    如果定义变量时没有指定初值,则变量被默认初始化。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响

    如果是内置类型的变量未被显示初始化,它的值由定义的位置决定,定义在任何函数体之外的变量被初始化为 0

    但是有一种例外,定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量时未定义的,如果试图拷贝或以其他形式访问此变量将引发错误

    每个类各自决定了其初始化对象的方式。绝大数类支持无须显示的初始化而定义对象。默认调用该类的默认构造方法

    1
    2
    3
    4
    5
    6
    int i1;  // 默认初始化,在函数体之外 (初始化为 0)
    int f(void)
    {
    int i2; // 不被初始化,如果使用此对象则报错
    }
    string empty; // empty 非显示的初始化为一个空串,调用的是默认构造函数
  2. 拷贝初始化

    使用等号 = 初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去,拷贝初始化通常使用拷贝构造函数来完成

    拷贝初始化不仅在使用 = 定义变量时会发生,在下列情况也会发生:

    • 将一个对象作为实参传递给一个非引用类型的形参
    • 从一个返回类型为非引用类型的函数返回一个对象
  3. 直接初始化

    实际上是要求编译器使用普通的函数来选择与提供的参数最匹配的构造函数

    1
    2
    3
    string str1(10,'9');  // 直接初始化
    string str2(str1); // 直接初始化
    string str3 = str1; // 拷贝初始化
  4. 值初始化,仅限于容器类

    1
    2
    3
    4
    5
    // 当只提供 vector 对象容纳的元素数量而去忽略元素的初始值,此时库会创建一个值初始化的元素初值,并把它赋予容器中的所有元素
    // 这个初值由 vector 对象中元素的类型决定,如果 vector 对象的元素是内置类型,比如 int,则元素初始值自动设置为 0,如果元素是某种类类型,比如 string,则元素由类默认初始化

    vector<int> v1(10); // 10 个元素,每个元素的初始化为 0
    vector<string> v2(10); // 10 个元素,每个元素都为空

大括号初始化生前生后

C++11 之前,内置类型(int 为例)和自定义类主要有以下几种初始化形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自定义 Test 类,下文不再重复声明
class Test{};

// 默认初始化
int x;
Test t1; // 调用默认构造函数

// 拷贝初始化
int x = 0;
Test t2 = t1; // 不是赋值操作,调用拷贝构造函数
t1 = t2; // 赋值操作,不是拷贝初始化,调用 operator= 函数

// 直接初始化
int x(0); // 初始值在圆括号内
Test t3(1, 2); // 调用带参构造函数
Test t4(); // 不是直接初始化,声明了一个名为 t4,不接受任何参数,返回 Test 类型的函数

虽然 C++03 提供了多样的对象初始化方式,但自定义类型对象却无法使用大括号进行初始化,也不能在使用 new[] 的时候初始化纯数据(Plain of Data, POD)数组。所以,C++11 提出了统一初始化语法:一种至少在概念上可以用于表达任何值的语法 —— 大括号初始化

1
2
3
4
5
6
7
int x{0};  // 初始值在大括号内

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

C++11 大括号初始化还可以应用于容器,终于可以摆脱 push_back() 调用了,C++11 中可以直观地初始化容器了:

1
2
3
// C++11 container initializer    
vector<string> vs = { "f", "s", "t" };
map<string, string> singers = { { "Gaga", "+1 (212) 555-7890" }, { "Knowles", "+1 (212) 555-0987" } };

此外,C++11 中,类的数据成员在申明时可以直接赋予一个默认值,大括号 {} 可以,等号 = 可以,但是,圆括号 () 不可以

1
2
3
4
5
6
7
class Test {
private:
// C++11 only
int x{ 0 }; // x 的默认初始值为 0
int y = 0; // 同上
int z( 0 ); // 报错
}

另一方面,不可拷贝对象 (例如,std::atomic) 可以用大括号和圆括号初始化,但不能用等号

1
2
3
std::atomic<int> ai1{ 0 };  // 可以
std::atomic<int> ai2( 0 ); // 可以
std::atomic<int> ai3 = 0; // 报错

并且,当大括号初始化用于内置类型的变量时,如果初始值存在丢失信息的风险,则编译器将报错

1
2
3
doubel ld = 3.14;
int a {ld}; // 报错,存在信息丢失风险
int b (ld); // 正确

大括号初始化的缺点

大括号初始化这种语法这样一举多得,那为什么不用大括号初始化语法替代其他呢?因为缺点,它有时会显现令人惊讶的的行为,这些行为的出现是因为与 std::initializer_list 混淆了

在构造函数中,只要形参不带有 std::initializer_list,圆括号和大括号行为一致:

1
2
3
4
5
6
7
8
9
10
11
class Test {
public:
Test(int i, bool b);
Test(int i, double d);
...
};

Test t1(10, true); // 调用第一个构造函数
Test t2{10, true}; // 调用第一个构造函数
Test t3(10, 5.0); // 调用第二个构造函数
Test t4{10, 5.0}; // 调用第二个构造函数

但是,如果构造函数的形参带有 std::initializer_list,调用构造函数时大括号初始化语法会强制使用带 std::initializer_list 参数的重载构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
class Test {
public:
Test(int i, bool b);
Test(int i, double d);
Test(std::initializer_list<long double> il);
...
};

Test t1(10, true); // 使用圆括号,调用第一个构造函数
Test t2{10, true}; // 使用大括号,强制调用第三个构造函数,10 和 true 被转换为 long double
Test t3(10, 5.0); // 使用圆括号,调用第二个构造函数
Test t4{10, 5.0}; // 使用大括号,强制调用第三个构造函数,10 和 5.0 被转换为 long double

即使是正常的拷贝构造和赋值构造也可以被带有 std::initializer_list 的构造函数劫持:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test {
public:
Test(int i, bool b);
Test(int i, double d);
Test(std::initializer_list<long double> il);
operator float() const; // 支持隐式转换为 float 类型
...
};

Test t5(t4); // 使用圆括号,调用拷贝构造函数
Test t6{t4}; // 使用大括号,调用第三个构造函数;先把 t4 转换为 float,再把 float 转换为 long dobule
Test t7(std::move(m4)); // 使用圆括号,调用移动构造函数
Test t8{std::move(m4)}; // 使用大括号,调用第三个构造函数,理由同 t6

编译器用带有 std::initializer_list 构造函数匹配大括号初始值的决心是如此的坚定,即使带有 std::initializer_list 的构造函数是无法调用的,编译器也会忽略另外两个构造函数(第二个还是参数精确匹配的):

1
2
3
4
5
6
7
8
9
class Test {
public:
Test(int i, bool b);
Test(int i, double d);
Test(std::initializer_list<bool> il); // long double 改为 bool
...
};

Test t{10, 5.0}; // 报错,因为发生范围窄化转换;编译器会忽略另外两个构造函数(第二个还是参数精确匹配的)

只有当大括号内的值无法转换为 std::initializer_list 元素的类型时,编译器才会使用正常的重载选择方法:

1
2
3
4
5
6
7
8
9
10
11
12
class Test {
public:
Test(int i, bool b);
Test(int i, double d);
Test(std::initializer_list<std::string> il); // bool 改为 std::string
...
};

Test t1(10, true); // 使用圆括号,调用第一个构造函数
Test t2{10, true}; // 使用大括号,不过调用第一个构造函数,因为无法转换为 string
Test t3(10, 5.0); // 使用圆括号,调用第二个构造函数
Test t4{10, 5.0}; // 使用大括号, 不过调用第二个构造函数,因为无法转换为 string

一个有趣的边缘情况:一个大括号内无参的实例化,是调用默认构造,还是调用带 std::initializer_list 的构造函数?

1
2
3
4
5
6
7
8
9
10
11
12
class Test {
public:
Test();
Test(std::initializer_list<int> il);
...
};

// 正确答案是调用默认构造,一个空的大括号表示的是没有参数,而不是一个空的 std::initializer_list
Test w2{};

// 如果想要调用带 std::initializer_list 的构造函数,需要把大括号作为参数,即把空的大括号放在圆括号内或者大括号内:
Test w4({});

be slow to promise and quick to perform.