第8章 非静态数据成员默认初始化(C++11 C++20)

在C++11以前,对非静态数据成员初始化需要用到初始化列表,当类的数据成员和构造函数较多时,编写构造函数会是一个令人头痛的问题:

class X {
public:
  X() : a_(0), b_(0.), c_("hello world") {}
  X(int a) : a_(a), b_(0.), c_("hello world") {}
  X(double b) : a_(0), b_(b), c_("hello world") {}
  X(const std::string &c) : a_(0), b_(0.), c_(c) {}

private:
  int a_;
  double b_;
  std::string c_;
};

在上面的代码中,类X有4个构造函数,为了在构造的时候初始化非静态数据成员,它们的初始化列表有一些冗余代码,而造成的后果是维护困难且容易出错。为了解决这种问题,C++11标准提出了新的初始化方法,即在声明非静态数据成员的同时直接对其使用=或者{}(见第9章)初始化。在此之前只有类型为整型或者枚举类型的常量静态数据成员才有这种声明默认初始化的待遇:

class X {
public:
  X() {}
  X(int a) : a_(a) {}
  X(double b) : b_(b) {}
  X(const std::string &c) : c_(c) {}

private:
  int a_ = 0;
  double b_{ 0. };
  std::string c_{ "hello world" };
};

以上代码使用了非静态数据成员默认初始化的方法,可以看到这种初始化的方式更加清晰合理,每个构造函数只需要专注于特殊成员的初始化,而其他的数据成员则默认使用声明时初始化的值。比如X(const std::string c)这个构造函数,它只需要关心数据成员c_的初始化而不必初始化a_b_。在初始化的优先级上有这样的规则,初始化列表对数据成员的初始化总是优先于声明时默认初始化。

最后来看一看非静态数据成员在声明时默认初始化需要注意的两个问题。

1.不要使用括号()对非静态数据成员进行初始化,因为这样会造成解析问题,所以会编译错误。

2.不要用auto来声明和初始化非静态数据成员,虽然这一点看起来合理,但是C++并不允许这么做。

struct X {
  int a(5);     // 编译错误,不能使用()进行默认初始化
  auto b = 8;   // 编译错误,不能使用auto声明和初始化非静态数据成员
};

在C++11标准提出非静态数据成员默认初始化方法之后,C++20标准又对该特性做了进一步扩充。在C++20中我们可以对数据成员的位域进行默认初始化了,例如:

struct  S {
  int y : 8 = 11;
  int z : 4 {7};
};

在上面的代码中,int数据的低8位被初始化为11,紧跟它的高4位被初始化为7。

位域的默认初始化语法很简单,但是也有一个需要注意的地方。当表示位域的常量表达式是一个条件表达式时我们就需要警惕了,例如:

int a;
struct S2 {
    int y : true ? 8 : a = 42;
    int z : 1 || new int { 0 };
};

请注意,这段代码中并不存在默认初始化,因为最大化识别标识符的解析规则让=42{0}不可能存在于解析的顶层。于是以上代码会被认为是:

int a;
struct S2 {
    int y : (true ? 8 : a = 42);
    int z : (1 || new int { 0 });
};

所以我们可以通过使用括号明确代码被解析的优先级来解决这个问题:

int a;
struct S2 {
  int y : (true ? 8 : a) = 42;
  int z : (1 || new int){ 0 };
};

通过以上方法就可以对S2::yS2::z进行默认初始化了。

非静态数据成员默认初始化在一定程度上解决了初始化列表代码冗余的问题,尤其在类中数据成员的数量较多或类重载的构造函数数量较多时,使用非静态数据成员默认初始化的优势尤其明显。另外,从代码的可读性来说,这种初始化方法更加简单直接。