C++列表初始化

Share on:
1000 Words | Read in about 5 Min | View times

Overview

在传统C++中,只能对普通数组和POD类型使用列表初始化,适用范围非常有限。在现代C++中,统一了初始化方式,任何类型对象的初始化都可以使用列表初始化了。本节内容我们讲解列表初始化的各项使用细节。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

统一的初始化方法

列表初始化

在传统C++中,只能对普通数组和POD类型使用列表初始化。从C++11开始,列表初始化扩展到任何类型对象的初始化。我们通过几个例子直观地了解一下什么是列表初始化:

  • 普通数组列表初始化
1int arr[] = { 1, 2, 3 };
  • POD列表初始化
1struct A {
2    int x;
3    int y;
4};
5
6A a = { 1, 2 };
  • 类对象初始化
 1class B {
 2public:
 3    B(int a) : m_a(a) { std::cout << "B::B(int)" << std::endl; }
 4private:
 5    B(const B& b) : m_a(b.m_a) { std::cout << "B::B(const B&)" << std::endl; }
 6    int m_a;
 7};
 8
 9int main() {
10    B b1(10); //B::B(int)
11    //B b2 = 10; //error: ‘B::B(const B&)’ is private within this context
12    B b3 = { 10 }; //B::B(int)
13    B b4{ 10 }; //B::B(int)
14    return 0;
15}

b2的写法其实是先将用10隐式创建一个B类的临时对象,再用拷贝构造函数创建了对象b2。但是拷贝构造函数是私有的,所以编译器报错。而最后两种写法都是列表初始化。

  • 代替圆括号进行初始化
1int* a = new int { 10 };
2double b = double { 3.14 };
3std::string c = new std::string { "hello world" };
4int* arr = new int[] { 1, 2, 3 };

综合这几个例子,直接在变量名后面加上初始化列表(一般表现形式为一对花括号)进行对象的初始化,就是列表初始化。

非聚合类型的初始化

然而列表初始化也是有一些特殊限制的。列表初始化是否可以完成,取决于这个类是否聚合类型(aggregate)。所谓聚合类型,是指拥有以下特点的类型:

  • 无自定义构造函数

  • privateprotected的非静态数据成员

  • 无基类

  • 无虚函数

  • {}=直接进行类内初始化的非静态数据成员

存在自定义构造函数

 1struct C {
 2    int x;
 3    int y;
 4    C(int a, int b) {}
 5};
 6
 7int main() {
 8    C c { 1, 2 };
 9    std::cout << "c.x = " << c.x << ", c.y = " << c.y << std::endl; //c.x = 0, c.y = 954540288
10    return 0;
11}

类中拥有自定义构造函数时,类中的数据成员只能通过构造函数来初始化,此时列表初始化的数据没有正常传递给这个构造函数。

正确的做法应该是:

 1struct C {
 2    int x;
 3    int y;
 4    //C(int a, int b) {}
 5    C(int a, int b) : x(a), y(b) {}
 6};
 7
 8int main() {
 9    C c { 1, 2 };
10    std::cout << "c.x = " << c.x << ", c.y = " << c.y << std::endl; //c.x = 1, c.y = 2
11    return 0;
12}

类中包含private或protected的非静态数据成员

 1struct D {
 2    int x;
 3    int y;
 4protected:
 5    int z;
 6};
 7
 8int main() {
 9    D d { 1, 2 };
10    std::cout << "d.x = " << d.x << ", d.y = " << d.y << std::endl; //error: no matching function for call to ‘D::D(<brace-enclosed initializer list>)’
11    return 0;
12}

含有privateprotected的非静态数据成员时,列表初始化会编译失败。

如果包含privateprotected静态数据成员呢?

 1struct D {
 2    int x;
 3    int y;
 4protected:
 5    static int z;
 6};
 7
 8int main() {
 9    D d { 1, 2 };
10    std::cout << "d.x = " << d.x << ", d.y = " << d.y << std::endl; //d.x = 1, d.y = 2
11    return 0;
12}

没有编译失败,而且也成功进行了列表初始化。

含有基类

 1struct Base {};
 2struct E : public Base {
 3    int x;
 4    int y;
 5};
 6
 7int main() {
 8    E e { 1, 2 };
 9    std::cout << "e.x = " << e.x << ", e.y = " << e.y << std::endl; //error: no matching function for call to ‘E::E(<brace-enclosed initializer list>)’
10    return 0;
11}

含有基类的列表初始化也会失败。

含有虚函数

 1struct F {
 2    int x;
 3    int y;
 4    virtual void foo() {}
 5};
 6
 7int main() {
 8    F f { 1, 2 };
 9    std::cout << "f.x = " << f.x << ", f.y = " << f.y << std::endl; //error: no matching function for call to ‘F::F(<brace-enclosed initializer list>)’
10    return 0;
11}

含有虚函数的列表初始化也会失败。

含有{}或者=的类内初始化的非静态数据成员

 1struct G {
 2    int x;
 3    int y = 10;
 4};
 5
 6int main() {
 7    G g { 1, 2 };
 8    std::cout << "g.x = " << g.x << ", g.y = " << g.y << std::endl; //error: no matching function for call to ‘G::G(<brace-enclosed initializer list>)’
 9    return 0;
10}

std::initializer_list

从聚合类型的列表初始化限制中可以看出,编译器报错的理由都提到了未提供initializer list的构造函数。

std::initializer_list是C++11提供的轻量级类模板,它的参数是可变参数,可以接受任意长度的相同类型的数据,广泛应用于STL容器的构造函数中:

1int arr[] = { 1, 2, 3, 4, 5 };
2std::map<int, int> mp { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
3std::list<std::string> ml { "hello", "world", "zhxilin" };
4std::vector<double> mv { 0.0, 0.1, 0.2, 0.3, 0.4, 0.5 };

这些STL容器都提供了接受std::initializer_list<T>作为参数的模板构造函数。

我们可以仿照STL容器的实现方式,对非聚合类型进行改造,使得支持列表初始化:

 1struct H {
 2    int x;
 3    int y;
 4    H(std::initializer_list<int> list) {
 5            auto it = list.begin();
 6            if (it != list.end())
 7                    x = *it;
 8            it++;
 9
10            if (it != list.end())
11                    y = *it;
12    }
13};
14
15int main() {
16    H h1{1};
17    std::cout << "h1.x = " << h1.x << ", h1.y = " << h1.y << std::endl; //h1.x = 1, h1.y = 21995
18    H h2{1,2};
19    std::cout << "h2.x = " << h2.x << ", h2.y = " << h2.y << std::endl; //h2.x = 1, h2.y = 2
20    H h3{1,2,3};
21    std::cout << "h3.x = " << h3.x << ", h3.y = " << h3.y << std::endl; //h3.x = 1, h3.y = 2
22    return 0;
23}

从运行结果可以看出,当std::initializer_list提供足够数量的参数满足类的数据成员数量时,都能全部初始化。

std::initializer_list具有以下特点:

  • 它是一个轻量级的容器类型,内部定义了容器所需的迭代器

  • 它可以接受任意长度的初始化列表,但是元素必须是要相同的或者可以转换为T类型

  • 它只有三个成员接口:begin()end()、·size()

  • 它只能被整体的初始化和赋值,遍历只能通过begin()end()迭代器来进行,遍历取得的数据是只读的,不可修改

  • 它保存的数据类型是T类型的引用,要注意变量的生命周期

列表初始化防止类型收窄

列表初始化还有一个好处,就是禁止类型收窄。所谓类型收窄,指的是将大范围的数据类型隐式转换为小范围的数据类型。比如以下情形都属于类型收窄:

  • 从浮点类型到整数类型的转换

  • long doubledoublefloat的转换,以及从doublefloat的转换,除非源是常量表达式且不发生溢出

  • 从整数类型到浮点类型的转换,除非源是其值能完全存储于目标类型的常量表达式

  • 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换,除非源是其值能完全存储于目标类型的常量表达式

来看以下例子:

 1int main() {
 2    int a = 3.14; //ok
 3    int b { 3.14 }; //error: narrowing conversion of ‘3.1400000000000001e+0’ from ‘double’ to ‘int’
 4
 5    float c = 1e50; //ok
 6    float d {1e50}; //error: narrowing conversion of ‘1.0000000000000001e+50’ from ‘double’ to ‘float’
 7
 8    float e = (unsigned long long)-1; //ok
 9    float f {(unsigned long long)-1}; //error: narrowing conversion of ‘18446744073709551615’ from ‘long long unsigned int’ to ‘float’
10    float g = (unsigned long long)1; //ok
11    float h {(unsigned long long)1}; //ok
12
13    const int i = 1000;
14    const int j = 2;
15
16    char k = i; //ok
17    char l {i}; //error: narrowing conversion of ‘1000’ from ‘int’ to ‘char’
18
19    char m = j; //ok
20    char m = {j}; //ok,因为是常量表达式,且其值能完全存储。如果去掉const属性,也会报错
21
22    return 0;
23}

传统C++允许类型收窄,这将导致一些隐藏错误非常难以排查。现代C++从根源杜绝了类型收窄的发生。所以建议以后对于对象的初始化尽量使用列表初始化进行。

Prev Post: 『C++ auto关键字』
Next Post: 『C++ STL标准库概览』