调用虚基类的重载构造函数

12

有没有(实际可行的)方法来绕过正常的(虚拟)构造函数调用顺序?

示例:

class A
{
    const int i;

public:
    A()
      : i(0)
    { cout << "calling A()" << endl; }

    A(int p)
      : i(p)
    { cout << "calling A(int)" << endl; }
};

class B
    : public virtual A
{
public:
    B(int i)
      : A(i)
    { cout << "calling B(int)" << endl; }
};

class C
    : public B
{
public:
    C(int i)
      : A(i), B(i)
    { cout << "calling C(int)" << endl; }
};

class D
    : public C
{
public:
    D(int i)
      : /*A(i), */ C(i)
    { cout << "calling D(int)" << endl; }
};


int main()
{
    D d(42);
    return 0;
}

输出:

调用 A()
调用 B(int)
调用 C(int)
调用 D(int)

我想要的是类似于这样的东西:

调用 A(int)
调用 B(int)
调用 C(int)
调用 D(int)


正如你所看到的,这里涉及到虚拟继承,导致D的构造函数首先调用A的构造函数,但由于没有提供参数,它调用了A()。有一个需要初始化的const int i,所以我有一个问题。

我想要做的是隐藏C的继承细节,这就是我寻找一种避免在D(和每个派生)构造函数的初始化列表中调用A(i)的方式。[编辑] 在这种特殊情况下,我可以假设C只有非虚拟单继承子类(如D)[/编辑]

[编辑]

虚基类在任何非虚基类之前初始化,因此只有最派生类才能初始化虚基类。 - James McNellis

这正是重点,我不想让最派生类调用虚基类构造函数。 [/编辑]

考虑以下情况(在上面的代码示例中未表示):

  A
 / \
B0  B1
 \ /
  C
  |
  D  

我理解为什么在实例化C时,C必须调用A的构造函数(模棱两可性),但是在实例化D时为什么也要调用它呢?


1
我不相信你的代码示例与你提供的输出匹配。你确定你使用指令“D d;”实例化了d吗? - Benoît
抱歉,我忘记了参数...现在是D d(42)。谢谢。 - dyp
好的,这样更公平 :-) 我可以问一下为什么你想要使用“可怕的钻石”架构吗?你不能以其他方式重新组织你的代码吗? - Benoît
是的,这总是一个可能性。当真的没有可行的方法时,我会选择重新设计。 - dyp
@Benoît 因为“钻石”并不可怕,它是一种非常好的工具。 - curiousguy
4个回答

7
很遗憾,您总是需要从最派生的类调用虚基类构造函数。
这是因为您说虚基类在对象实例中被所有派生自它的类共享。由于构造函数只能针对给定对象实例调用一次,因此必须在最派生的类中显式调用构造函数,因为编译器不知道有多少类共享虚基类(引用自《C++程序设计语言》第三版15.2.4.1节,可能表述不够准确)。这是因为编译器将从最基础类的构造函数开始,逐步向最派生类工作。直接继承虚基类的类不会调用其虚基类的构造函数,因此必须显式调用虚基类的构造函数。

直接继承虚拟基类的类,按标准不会调用其虚拟基类的构造函数,因此必须显式调用它。这对于类B是正确的,然后类C可以调用它。但由于C不是最终派生类,所以D必须调用它。有什么办法让C调用它吗?D通过单一非虚拟继承直接继承自C。 - dyp
@DyP:假设我后来添加了一个从C派生的类E,然后使D同时从C和E多重继承(即使我没有将C设置为虚拟),那么哪个C会调用A的构造函数? - diverscuba23
抱歉,我编辑了问题。我可以假设 C 没有多重继承。 - dyp
@DyP:针对您的特定情况,这不会有任何问题,但该语言是为包括我所举的例子在内的一般情况编写的。确保虚基类的构造函数在所有可能的情况下仅被调用一次的最简单方法是使得最派生类负责调用它。 - diverscuba23
我同意并理解为什么设计成这样。这就是为什么我请求绕过此规则的方法(如使用双阶段初始化或继承等)。 - dyp
直接继承虚基类的类,与间接继承有本质区别。对于非虚拟继承,直接和间接继承是必须区分的;但对于虚拟继承来说,这个区别就不那么重要了。你可以认为虚基类总是“直接”的(除了在访问控制方面存在路径并且需要沿着路径检查访问控制的情况下)。 - curiousguy

2
我理解为什么在实例化C时需要调用A的构造函数(存在歧义),但是在实例化D时为什么也要调用呢?
与C需要调用它的原因相同。这不是一个歧义问题,而是因为A的构造函数只能被调用一次(因为它是虚基类)。
如果您希望C能够初始化A的构造函数,那么如果类D继承C和另一个最终继承A的类会怎样呢?

如果D类继承了两个C类,那我不是需要虚拟继承自C类吗? - dyp
@DyP:是的,两个C的问题很公正。我已经从我的答案中删除了它。当然,这个观点仍然存在。 - Troubadour
@DyP:不一定,如果C语言可以支持在一个对象中有多个副本,它就不需要是虚拟的。虽然很少会有这样的情况发生,但使用该语言实现也是可能的。 - diverscuba23
“这不是歧义的问题[...]”,是的,我想说如果在实例化C时B调用它是无意义的,因为C可以从B0和B1(看菱形图)派生出来。我的具体问题涉及D,它是C的单一非虚拟子类,以及D的其他(非虚拟、单继承)子类。我编辑了问题。 - dyp

0

这就是规则。有关覆盖虚函数的规则和构造虚基类子对象的规则。尽管在概念上两者非常相似,但它们遵循完全不同的规则,原因是:覆盖虚函数是显式的。对于默认构造函数来说,调用构造函数是隐式的。

虚基类中的虚函数只需要一个最终的覆盖者,即覆盖所有其他覆盖者的覆盖者。(非虚基类中的虚函数不可能有两个覆盖者,其中一个不覆盖另一个。)

但是,虚基类构造函数总是从最派生类中调用,并且通常以隐式形式调用,不必在ctor-init-list中提及虚基类,因为大多数设计为用作虚基类的类都是“纯接口”,没有数据成员和用户初始化。


-1
在Parashift的C++ FAQ Lite中,这个问题已经概述了。

我认为作者的问题是为什么你总是需要从最派生的类中调用虚基类的构造函数,而不是他所提供的例子中遇到的特定问题。这个例子只是为了清楚地说明他的问题。 - diverscuba23
@diverscuba23:我花了一些时间搜索才理解问题所在。现在我明白了,但不理解会导致这种情况的原因。 - Shamster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接