C++11 列表初始化

初始化列表

在C++98中,标准允许使用花括号”{}”对数组元素进行统一的集合(列表)初始值设定,比如:

int arr[5] = {0};
int arr[] = {1, 2, 3, 4} ;

这些都是合法的表达式。不过一些自定义类型,却无法享受这样便利的初始化。通常,如标准程序库中的vector这样的容器,总是需要声明对象-循环初始化这样的重复动作,这对于使用模板的泛型编程无疑是非常不利的。

快速初始化中,我们看到了C++11对类成员的快速就地初始化。有一种初始化形式就是使用花括号的集合(列表)初始化。而事实上,在C++11中,集合(列表)的初始化已经成为C++语言的一个基本功能,在C++11中,这种初始化的方法被称为“初始化列表”(initializer list)。让我们来看看代码清单3-29所示的这个例子。
代码清单3-29

  #include <vector>
  #include <map>
  using namespace std;
  int a[] = {1, 3, 5};          // C++98通过,C++11通过
  int b[] {2, 4, 6};            // C++98失败,C++11通过
  vector<int> c{1, 3, 5};      // C++98失败,C++11通过
  map<int, float> d =
        {{1, 1.0f}, {2, 2.0f} , {5, 3.2f}}; // C++98失败,C++11通过

我们看到了变量b、c、d,在C++98的情况下均无法通过编译,在C++11中,却由于列表初始化的存在而可以通过编译。这里,列表初始化可以在“{}”花括号之前使用等号,其效果与不带使用等号的初始化相同。

这样一来,自动变量和全局变量的初始化在C++11中被丰富了。程序员可以使用以下几种形式完成初始化的工作:

  • 等号“=”加上赋值表达式(assignment-expression),比如int a = 3 + 4
  • 等号“=”加上花括号式的初始化列表,比如int a = {3 + 4}
  • 圆括号式的表达式列表(expression-list),比如int a (3 + 4)
  • 花括号式的初始化列表,比如int a {3 + 4}

而后两种形式也可以用于获取堆内存new操作符中,比如:

  int * i = new int(1);
  double * d = new double{1.2f};

这在C++11中也是合法的表达式。

代码清单3-29中可能令读者比较惊讶的是,使用初始化列表对vector、map等非内置的复杂的数据类型进行初始化竟然也是可以的。进一步地,读者可能会猜测是否初始化列表是专属于内置类型、数组,以及标准模板库中容器的功能呢?

事实并非如此,如同我们所提到的,在C++11中,标准总是倾向于使用更为通用的方式来支持新的特性。标准模板库中容器对初始化列表的支持源自<initializer_list>这个头文件中initialize_list类模板的支持。程序员只要#include了<initializer_list>头文件,并且声明一个以initialize_list<T>模板类为参数的构造函数,同样可以使得自定义的类使用列表初始化。让我们来看一看代码清单3-30的例子。
代码清单3-30

#include <vector>
#include <string>
using namespace std;
enum Gender {boy, girl};
class People {
public:
    People(initializer_list<pair<string, Gender>> l) { // initializer_list的构造函数
        auto i = l.begin();
        for (;i != l.end(); ++i)
            data.push_back(*i);
    }
private:
    vector<pair<string, Gender>> data;
};
People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}};

我们为类People定义了一个使用initializer_list<pair<string, Gender>>模板类作为参数的构造函数。这里我们使用了C++11的auto关键字来自动类型推导以简化代码的编写。由于该构造函数的存在,ship2012声明就可以使用列表初始化了。事实上,编写一个列表初始化的构造函数并不困难。对于旧有的代码,列表初始化构造函数还常常可以调用已有的代码来实现。

同样的,函数的参数列表也可以使用初始化列表,如代码清单3-31所示。
代码清单3-31

#include <initializer_list>
using namespace std;
void Fun(initializer_list<int> iv){ }
int main() {
    Fun({1, 2});
    Fun({}); // 空列表
}

定义了一个可以接受初始化列表的函数Fun。同理,类和结构体的成员函数也可以使用初始化列表,包括一些操作符的重载函数。而在代码清单3-32所示的这个例子中,我们利用了初始化列表重载了operator[],并且重载了operator =以及使用辅助的数组。虽然这个例子比较复杂,但重载的效果还是能够让人感觉眼前一亮的。

代码清单3-32

#include <iostream>
#include <vector>
using namespace std;
class Mydata {
public:
    Mydata & operator [] (initializer_list<int> l)
    {
        for (auto i = l.begin(); i != l.end(); ++i)
            idx.push_back(*i);
        return *this;
    }
    Mydata & operator = (int v)
    {
        if (idx.empty() != true) {
            for (auto i = idx.begin(); i != idx.end(); ++i) {
                d.resize((*i > d.size()) ? *i : d.size());
                d[*i -1] = v;
            }
            idx.clear();
        }
        return *this;
    }
    void Print() {
        for (auto i = d.begin(); i != d.end(); ++i)
            cout << *i << " ";
        cout << endl;
    }
private:
    vector<int> idx;     // 辅助数组,用于记录index
    vector<int> d;
};
int main() {
    Mydata d;
    d[{2, 3, 5}] = 7;
    d[{1, 4, 5, 8}] = 4;
d.Print();   // 4 7 7 4 4 0 0 4
}

我们看到自定义类型Mydata拥有一个以前所有C++代码都不具备的功能,即可以在[]符号中使用列表,将设置数组中的部分为一个指定的值。在这里我们先把数组的第2、3、5位设为数值7,而后又将其1、4、5、8位设为数值4,最终我们得到数组的内容为“4 7 7 4 4 0 0 4”。读者可以自行分析一下代码的实现方式(这段代码比较粗糙,读者应该重点体会初始化列表带来的编程上的灵活性)。当然,由于内置的数组不能重载operator[],我们也就无法为其实现相应的功能。
此外,初始化列表还可以用于函数返回的情况。返回一个初始化列表,通常会导致构造一个临时变量,比如:

vector<int> Func() { return {1, 3}; }

当然,跟声明时采用列表初始化一样,列表初始化构造成什么类型是依据返回类型的,比如:

deque<int> Func2() { return {3, 5};}

上面的返回值就是以deque<int>列表初始化构造函数而构造的。而跟普通的字面量相同,如果返回值是一个引用类型的话,则会返回一个临时变量的引用。比如:

const vector<int>& Func1() { return {3, 5};}

这里注意,必须要加const限制符。该规则与返回一个字面常量是一样的。

防止类型收窄

使用列表初始化还有一个最大优势是可以防止类型收窄(narrowing)。类型收窄一般是指一些可以使得数据变化或者精度丢失的隐式类型转换。可能导致类型收窄的典型情况如下:

  • 从浮点数隐式地转化为整型数。比如:int a = 1.2,这里a实际保存的值为整数1,可以视为类型收窄。
  • 从高精度的浮点数转为低精度的浮点数,比如从long double隐式地转化为double,或从double转为float。如果这些转换导致精度降低,都可以视为类型收窄。
  • 从整型(或者非强类型的枚举)转化为浮点型,如果整型数大到浮点数无法精确地表示,则也可以视为类型收窄。
  • 从整型(或者非强类型的枚举)转化为较低长度的整型,比如:unsigned char = 1024,1024明显不能被一般长度为8位的unsigned char所容纳,所以也可以视为类型收窄。

值得注意的是,如果变量a从类型A转化为类型B,其值在B中也是可以被表示的,且再转化回类型A能获得原有的值的话,那么这种类型转换也不能叫作类型收窄。所以类型收窄也可以简单地理解为新类型无法表示原有类型数据的值的情况。事实上,发生类型收窄通常也是危险的,应引起程序员的注意。因此,在C++11中,使用初始化列表进行初始化的数据编译器是会检查其是否发生类型收窄的。
代码清单3-33

const int x = 1024;
const int y = 10;
char a = x;                         // 收窄,但可以通过编译
char* b = new char(1024);           // 收窄,但可以通过编译
char c = {x};                       // 收窄,无法通过编译
char d = {y};                       // 可以通过编译
unsigned char e {-1};               // 收窄,无法通过编译
float f { 7 };                       // 可以通过编译
int g { 2.0f };                     // 收窄,无法通过编译
float * h = new float{1e48};       // 收窄,无法通过编译
float i = 1.2l;                      // 可以通过编译

我们定义了a到i一共9个需要初始化的变量。可以看到,对于变量a和*b而言,由于其采用的是赋值表达符及圆括号式的表达式初始化,所以虽然它们的数据类型明显收窄(char通常取值范围为-128到127),却不会引发编译失败(事实上,在我们的实验机上会得到编译器的警告)。而使用初始化列表的情况则不一样。对于变量c,由于其类型收窄,则会导致编译器报错。而对于变量d来说,其初始化使用了常量值10,而10是可以由char类型表示的,因此这里不会发生收窄,编译可以通过。同样的情况还发生在变量f、i的初始化上。虽然初始化语句中的变量类型往往“大于”变量声明的类型,但是由于值在f、i中可以表示,还可以被转回原有类型不发生数据改变或者精度错误等,因此也不能算收窄。

比较容易引起疑问的是无符号类型的变量e。虽然按理说e如果再被转换为有符号数,其值依然是-1,但对于无符号数而言,并不能表示-1,因此这里我们也认为e的初始化有收窄的情况。另外,f和g的差别在于2.0f是一个有精度的浮点数值,通常可以认为,将2.0f转换成整型会丢失精度,所以g的声明也是收窄的。

在C++11中,列表初始化是唯一一种可以防止类型收窄的初始化方式。这也是列表初始化区别于其他初始化方式的地方。事实上,现有编译器大多数会在发生类型收窄的时候提示用户,因为类型收窄通常是代码可能出现问题的征兆。C++11将列表初始化设定为可以防范类型收窄,也就是为了加强类型使用的安全性。

总的来说,列表初始化改变了C++中对类型初始化的一些基本模式,将标准程序库跟语言拉得更近了。这样的做法有效地统一了内置类型和自定义类型的行为。这也是C++11设计所遵循的一个思想,即通用为本,专用为末。

酷客教程相关文章:

赞(0)

评论 抢沙发

评论前必须登录!