很多很多年前,还在北京读研时,我去 magnetar 寻求一个实习机会时,面我的北美小哥让我解释一下C++的菱形关系,好像当时没有答上来。
今天在 OceanBase 代码里又看到了菱形关系。多重继承在我们的代码中是很少出现的,居然能再次遇到可真不容易。
问题:下面的代码里,C 的构造函数为什么必须显式调用 Base(v)?
class Base { explicit A(int &) {} }
class A : virtual public Base { }
class B : virtual public Base { }
class C : public A, public B
{
C(int v) : Base(v), A(), B() {}
}
更进一步地,为什么下面比较简单的场景里 C 依然要显式调用 Base(v)?
class Base { explicit A(int &) {} }
class A : virtual public Base { }
class C : public A
{
C(int v) : Base(v), A() {}
}
Well,有了一些工作经验后会积累一些设计的思想,能够更好地站在设计者角度思考问题。回答上面两个问题就比较容易了:
C++ 设计者引入了一条简单规则:A、B 这样使用了 virtual 继承的对象作为中间类时,没有资格去实例化基类。
还是有点绕,更简单的思维:
A、B 这样使用了 virtual 继承的对象作为中间类时,不会为A、B 分配基类对象的结构。
只要不让他们去分配对象,那么自然就需要子类去分配对象,子类分配了对象,自然就需要主动调用 Base 的构造函数。
C++设计者面对菱形问题时,解决思维如下:
- 要避免给基类(Base)分配多份内存
- 所以子类 A、B 都不应该去给基类(Base)分配内存
- 所以内存只能由 A、B 的子类 C 去分配内存
- 所以 C 需要调用构造函数。
然后遇到一个特殊情况:
- 如果 Base 只有一个子类 A,A 只有一个子类 C,那么 A 能不能为 Base 分配内存呢?
- 这里如果考虑极致优化,此时可以让 A 给 Base 分配内存,C无需关注 Base。
- 但是这样做太特殊。所以设计者保持了一致的用户接口:Base 的内存必须由 C 分配。
|