cpp / 2023.02.03

C++11 の 委托与继承的构造函数(Delegating & Inherited constructors)

委托构造函数与继承构造函数并非独立的新构造函数,而是 C++11 新增的用于提高编码效率的特性。

委托构造函数继承构造函数并非独立的新构造函数,而是 C++11 新增的用于提高编码效率的特性。

委托构造函数

委托构造函数允许构造函数通过初始化列表调用同一个类的其他构造函数,相当于将自己的构造职责委托给了其它构造函数。目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。

 1// 不使用委托,显得十分臃肿且冗余
 2class A {
 3 public:
 4  A(char i_) {
 5    i = i_;
 6    j = 0;
 7    display();
 8  }
 9  A(char i_, int j_) {
10    i = i_;
11    j = j_;
12    display();
13  }
14 private:
15  char i;
16  int j;
17  void display();
18};
19
20// 使用委托,精简干练
21class A {
22 public:
23  A() { display(); }
24  A(char i_): A() { i = i_; j = 0; }    // 委托构造函数
25  A(char i_, int j_): A(i_) { j = j_; } // 委托构造函数
26 private:
27  char i;
28  int j;
29  void display();
30};

一个委托构造函数也有一个成员初始化列表和一个函数体,成员初始化列表只能包含一个其它构造函数,且参数列表必须与已有的构造函数匹配。初始化列表里不能再包含其它成员变量的初始化,只能在函数体中对变量进行赋值。

注意事项

委托构造不能形成循环,比如下面这种代码:

1class A {
2 public:
3  A(char i_): A(i_, 'c') {}
4  A(char i_, int j_): A(i_) { j = j_; }
5 private:
6  char i;
7  int j;
8};

A(int i_)A(int i_, char j_) 反复调用对方形成循环,这样会导致编译错误。

继承构造函数

子类为完成基类初始化,在 C++11 之前,需要须要在构造函数中显式声明,即在初始化列表调用基类的构造函数,从而完成构造函数的传递。如果基类拥有多个构造函数,那么子类也需要实现多个与基类构造函数对应的构造函数,比如:

 1class Base {
 2 public:
 3  Base() { display(); }
 4  Base(char i_): Base() { i = i_; j = 0; }
 5  Base(char i_, int j_): Base(i_) { j = j_; }
 6
 7 private:
 8  char i;
 9  int j;
10  void display() {
11    std::cout << i <<  " " << j << std::endl;
12  }
13};
14
15class Derived: public Base {
16 public:
17  Derived(char i_): Base(i_) {}
18  Derived(char i_, int j_): Base(i_, j_) {}
19};

如果仅仅是为了完成基类的初始化,那么这样的做法显得非常冗余,代码的书写开销高达 $O(n)$!

从 C++11 开始,我们可以直接使用 using Base::Base 的方式来将基类中的构造函数全继承到派生类中,而无需重复书写,比如:

 1class Base {
 2 public:
 3  Base() { display(); }
 4  Base(char i_): Base() { i = i_; j = 0; }
 5  Base(char i_, int j_): Base(i_) { j = j_; }
 6  // private 部分略
 7};
 8
 9class Derived: public Base {
10 public:
11  using Base::Base;
12};

和上面那种写法是等价的,比如使用 char 变量去初始化 Derived 类变量时,会调用 Base(char) 去初始化基类,然后调用 display() 打印输出。更巧妙的是,这是隐式声明继承的,即假设一个继承来的构造函数不被相关的代码使用,编译器不会为之产生真正的函数代码,这样比显式书写各种构造函数更加节省代码量。

注意事项

继承构造函数的注意事项较多,一一说明。

  1. 继承构造函数无法初始化派生类数据成员。这很显然,因为继承来的构造函数仅对基类进行初始化。如果要初始化派生类变量,有两种做法。一是使用 = / {} 对非静态成员就地初始化,二是额外书写构造函数,两种做法各有优劣,第一种减少了代码量,第二种更加灵活,需根据具体应用场景进行选择。
  2. 当派生类拥有多个基类时,如果多个基类中的部分构造函数的参数列表(中的类型与顺序)完全一致,那么派生类中的继承构造函数将产生冲突,比如:
 1class A {
 2 public:
 3  A(int i) {}
 4};
 5class B {
 6 public:
 7  B(int i) {}
 8};
 9class C: A, B {
10 public:
11  using A::A; // 等价于 C(int i): A(i) {}
12  using B::B; // 等价于 C(int i): B(i) {}
13  C(int i): A(i), B(i) {} // 应显式声明会产生冲突的构造函数,会将上面两个"等价于"覆盖,阻止了隐式生成对应的构造函数,避免了冲突
14};
  1. 若基类构造函数声明为 private,则派生类无法使用该构造函数;若为 public,即便 using 处于 private 中,也能使用。比如:
 1class A {
 2 public:
 3  A(int i) {}
 4 private:
 5  A(int i, int j) {}
 6};
 7
 8class B: A {
 9 private:
10  using A::A;
11};
12
13B p(1);    // OK! although using is private
14B q(1, 2); // ERROR! A(int, int) is private

网上好多文章提到两点:

一是若基类构造函数存在默认值,则无法继承该默认值;

二是若派生类是是从基类虚继承的,那么就不能在派生类中继承构造函数;

但这两点在实践中都被推翻,但我也无法判断是否是错,这两点存疑。代码如下:

 1class A {
 2 public:
 3  A(int a = 3, double b = 4): _a(a), _b(b){ display(); }
 4  void display() { std::cout << _a << " " << _b << std::endl; }
 5
 6 private:
 7  int _a;
 8  double _b;
 9};
10
11class B: virtual A {
12 public:
13  using A::A;
14};
15
16class C: virtual A {
17 public:
18  using A::A;
19};
20
21class D: B, C {
22  using B::B;
23  using C::C;
24};
25
26int main() {
27  B b(1);
28  D d();
29}
30// output:
31// 1 4
32// 3 4

从输出易得 BC 确实从 A 处继承来了构造函数,并且默认值也得到了继承。