C++11:统一的初始化和初始化列表

在 C++98/03 中,有很多中不同的变量初始化方式。例如:

int i = 0;
int j(0);
int k = int(0);
std::string s1("abc");
std::string s2 = std::string("abc");
std::string s3 = "abc";
int arr[3] = {1, 2, 3};

这些各种各样的初始化方法,有的效果相同,有的效果相同但要求不同,基本都有各自的适用范围和作用,没有一种可以通用的方式,为了解决这个问题,C++ 提出了一种统一初始化(Uniform Initialization)的方式,它使用列表初始化来实现。

列表初始化

在 C++98/03 中,只有普通数组和 POD 类型的数据才可以使用初始化列表的方式。例如:

int arr1[3] = {1, 2, 3};
int arr2[] = {1, 2, 3, 4};

struct Data
{
    int a;
    int b;
} d = {1, 2};

除了可以继续用在这里提到的 POD 类型和数组,C++11 大大扩展了这种写法的适用范围。首先对于普通变量也可以使用大括号来初始化:

int i{0};
int j = {1};

对于类的构造也可以使用大括号:

std::string s1{"abc"};
std::string s2 = {"def"};

对于接收多个参数的构造函数,这种方式同样适用:

struct Foo
{
    Foo(int i, double d, char c) { }
};

Foo f1{1, 2.0, 'a'};
Foo f2 = {1, 2.0, 'a'};

在以上这些例子中,有没有等号都是一样的。

并且这种方式也可以用于 new 操作。例如:

int *i = new int{256};
Foo *f = new Foo{1, 2.0, 'c'};

甚至还可以直接为堆上分配的动态数组进行初始化:

int *arr = new int[3] {1, 2, 3};

初始化列表对于复杂类型也可以嵌套,比如对与前面例子中的 struct Foo,可以这么写:

Foo arr1[3] = {{1, 2.0, 'a'}, {2, 3.0, 'b'}, {3, 4.0, 'c'}};
Foo *arr2 = new Foo[3] {{1, 2.0, 'a'}, {2, 3.0, 'b'}, {3, 4.0, 'c'}};

对于 STL 的容器,同样可以使用列表初始化。例如:

std::vector<int> vec = {1, 2, 3, 4};
std::list<double> ls = {1.0, 2.0, 3.0};
std::map<int, char> dict = {{1, 'a'}, {2, 'c'}, {3, 'd'}};

总结来看,C++11 通过这种大括号的列表初始化的方式,形成了一套比较统一的初始化写法,也能够使很多代码变得更优雅易读。

防止类型收窄

使用列表初始化除了形式上比较统一以外,还有一个好处是可以防止类型收窄。类型收窄指的是在隐式的类型转换时,丢失了数据精度或者超出了表示范围。比如:

char c = 1024; // 超出表示范围
int i = 1.1;   // 丢失数据精度

这两行代码并不会导致编译报错(可能有编译警告),但显然执行的结果与程序员的本意不符。使用列表初始化的方式可以避免无意中的类型收窄:

char c {1024}; // Error
int i {1.1};   // Error

在使用列表初始化的时候,如果编译器检测到发生了类型收窄,就会报告编译错误。

自定义初始化列表

前面的例子我们看到,STL 的容器模板类可以使用不定长的初始化列表的方式来进行初始化,十分方便。

std::vector<int> vec = {1, 2, 3, 4};
std::list<double> ls = {1.0, 2.0, 3.0};
std::map<int, char> dict = {{1, 'a'}, {2, 'c'}, {3, 'd'}};

其实这种不定长的初始化列表是通过 std::initializer_list 来实现的,这是 C++11 新引入的一个模板类,通过将 std::initializer_list 作为构造函数的参数,我们自己的结构也可以实现通过不定长的初始化列表构造。

struct Foo
{
    Foo(std::initializer_list<int> il)
        : count(il.size()), sum(0)
    {
        for (auto i = il.begin(); i < il.end(); ++i)
            sum += *i;
    }

    int count;
    int sum;
};

int main()
{
    Foo f1 = {1, 2, 3}; // f1.count == 3, f1.sum == 6
    Foo f2 = {1, 3, 5, 7, 9}; // f2.count == 5, f2.sum == 25
}

在使用 std::initializer_list 时需要指定它的模板参数,也就是能够接受的参数的类型。在 Foo 构造时,大括号的列表参数会被转换为 std::initializer_list 传入构造函数,std::initializer_list 非常简单,它只有三个接口,size()用来获取参数列表的长度,begin()end() 分别获取开始和结尾的迭代器。

实际上,std::initializer_list 不仅可以用作构造初始化,它可以作为任何函数的参数,所以我们也可以设计出能够接受不定长参数列表的函数接口。例如:

struct Foo
{
    Foo(std::initializer_list<int> il)
        : count(il.size()), sum(0)
    {
        for (auto i = il.begin(); i < il.end(); ++i)
            sum += *i;
    }

    void add(std::initializer_list<int> il)
    {
        count += il.size();
        for (auto i = il.begin(); i < il.end(); ++i)
            sum += *i;
    }

    int count;
    int sum;
};

int main()
{
    Foo f1 = {1, 2, 3}; // f1.count == 3, f1.sum == 6
    f1.add({4, 5}); // f1.count == 5, f1.sum == 15
}

成员变量就地初始化

C++11 标准引入了一条新的规则,允许非静态的成员变量在声明的时候就地初始化。

在 C++98 中,只有整形的静态成员常量才能在声明的时候使用等号=就地初始化,而普通的非静态成员变量只能在构造函数中进行初始化。例如:

struct Foo
{
    static const int a = 1; // OK: 整形的静态常量
    int b = 2; // Error in C++98
    Foo() : b(2) { } // b只能在构造函数中初始化 
};

在 C++11 中,对于所有的非静态成员变量,都允许就地初始化,可以使用普通等号赋值的形式,也可以使用初始化列表的形式。例如:

struct Foo
{
    int a = 0;
    int b {1};
    std::string name = "unknown";
    std::vector<int> v { 1, 2, 3, 4 };
}

对于有多个成员变量和多个构造函数的结构,通过成员就地初始化的方式可以简化其构造函数的编写。例如:

// C++98的写法
struct Foo
{
    // 每个构造函数都必须初始化所有成员变量
    Foo(int i) : data(i), ok(false), name("unknown") { }
    Foo(bool b) : data(0), ok(b), name("unknown") { }
    Foo(const char *cstr) : data(0), ok(false), name(cstr) { }

    int data;
    bool ok;
    std::string name;
};

// C++11成员就地初始化
struct Foo
{
    // 只需要在构造函数中初始化相应的成员,其余成员使用就地初始化
    Foo(int i) : data(i) { }
    Foo(bool b) : ok(b) { }
    Foo(const char *cstr) : name(cstr) { }

    int data = 0;
    bool ok = false;
    std::string name = "unknown";
};

需要注意,只有非静态成员变量才能就地初始化,静态的成员变量仍然需要在源文件中去定义。

struct Foo
{
    static int count = 0; // Error
    // 必须在cpp文件中定义:int Foo::count = 0;
}

另外需要注意的是,构造函数中的初始化可以“覆盖”成员变量的就地初始化,当一个成员变量同时存在就地初始化和构造函数的初始化时,最终效果为构造函数的效果。例如:

class Foo
{
public:
    Foo() : x(0), y(0), z(0) { }
private:
    int x = 1;
    int y = 1;
    int z = 1;
};

Foo f 的执行结果,成员 x, y, z 的值均为 0。

使用 Hugo 构建
主题 StackJimmy 设计