为什么使用虚拟基类会更改复制构造函数的行为

时间:2016-01-25 13:07:53

标签: c++

在以下程序中,当B从A虚拟派生并且C(非B)的实例被复制时,不会复制a成员变量。

#include <stdio.h>

class A {
public:
    A() { a = 0; printf("A()\n"); }

    int a;
};

class B : virtual public A {
};

class C : public B {
public:
    C() {}
    C(const C &from) : B(from) {}
};

template<typename T>
void
test() {
    T t1;
    t1.a = 3;
    printf("pre-copy\n");
    T t2(t1);
    printf("post-copy\n");
    printf("t1.a=%d\n", t1.a);
    printf("t2.a=%d\n", t2.a);
}

int
main() {
    printf("B:\n");
    test<B>();

    printf("\n");

    printf("C:\n");
    test<C>();
}

输出:

B:
A()
pre-copy
post-copy
t1.a=3
t2.a=3

C:
A()
pre-copy
A()
post-copy
t1.a=3
t2.a=0

请注意,如果B通常来自A(您删除virtual),则会复制a

为什么没有a复制第一种情况(test<C>(),其中B实际上来自A?

4 个答案:

答案 0 :(得分:20)

虚拟继承是一个有趣的野兽,因为复制结构不是继承的#34;以与正常情况相同的方式。您的A基础是默认构造的,因为您没有明确地复制它:

class C : public B {
public:
    C() {}
    C(const C &from) : A(from), B(from) {}
};

答案 1 :(得分:15)

理解虚拟继承的最佳方法是了解虚拟继承的类总是由派生类最多的类进行子类化。

换句话说,示例中的类层次结构以某种方式结束:

class A {
};

class B {
};

class C : public B, public A {
};

从某种抽象的角度来看,这就是在这里发生的事情。 “派生程度最高”或“顶级”类成为其层次结构中所有虚拟类的直接“父级”。

因此,您正在定义C的复制构造函数,复制构造B,但由于A不再是B的子类,因此不会复制构造A ,因此你看到的行为。

请注意,我刚才所说的所有内容仅适用于C类。 B类本身就是根据您的预期派生自A。只是当您使用虚拟超类声明类的其他子类时,所有虚拟超类都“浮动”到新定义的子类。

答案 2 :(得分:12)

C ++ 11标准在12.6.2 / 10中说:

  

在非委托构造函数中,初始化继续进行   以下顺序:
   - 首先,仅适用于派生类最多的构造函数(1.8),虚拟基类按顺序初始化   它们出现在深度优先的从左到右的遍历中   基类的非循环图,其中“从左到右”是基数的顺序   派生类中基类的外观   基本符列表。
   - [直接基类等......]

这就是说,基本上 - 最派生的类负责以它定义它的任何方式进行初始化(在OP中:它不会导致默认初始化)。标准中的后续示例具有与此处的OP类似的场景,只有ctor的int参数;只调用虚拟基础的默认ctor,因为没有明确的&#34; mem-initializer&#34;在最派生的类中提供了虚拟基础。

有意思的是,虽然也没有直接在这里申请,但也是12.6.2 / 7:

  

mem-initializer [可能A()中的B(): A() {}。 -pas]其中mem-initializer-id表示虚拟基础   在执行任何类的构造函数时,将忽略class   不是派生最多的阶级。

(我觉得非常难熬。语言基本上说'#34;我不在乎你编码的内容,我会忽略它。&#34;没有那么多的地方可以做到这一点,违反as-if。)一个不是最派生类的构造函数将是B()。这句话不直接适用于此,因为B中没有明确的构造函数,因此也没有mem-initializer。但是,虽然我无法在标准中找到相应的措辞,但必须假设(且一致)相同的规则适用于生成的复制构造函数。

为了完整起见,Stroustrup在&#34; C ++编程语言&#34; (4.ed,21.2.5.1)关于在某个地方有一个虚拟基地V的最衍生的D类:

  

V未被明确提及作为D的基础这一事实无关紧要。对虚拟基础的了解以及初始化它的义务&#34;起泡&#34;到最派生的类。 虚拟基础始终被视为其派生类最直接的基础。

这正是Sam Varshavchik在之前的帖子中所说的。

然后Stroustrup继续讨论从D派生DD类使得必须将V的初始化移动到DD,这可能是一个令人讨厌的问题。这应该鼓励我们不要过度使用虚拟基类。&#34;

我发现基类保持未初始化(嗯,更确切地说:默认初始化)是非常模糊和危险的,除非最派生的类明确地做了什么。

最衍生类的作者必须深入研究 他/她可能没有兴趣或记录的继承层次结构,并且不能依赖于例如图书馆/他用来做正确的事情(图书馆不能)。

我也不确定我同意其他帖子中给出的理由(&#34;各个中间类应该执行初始化?&#34;)。该标准有一个明确的初始化顺序概念(&#34;深度优先从左到右遍历&#34;)。无法强制要求遇到哪个虚拟继承自基类的第一个类执行初始化?

12.8 / 15中规定了默认副本ctor 初始化虚拟基础的有趣事实:

  

以这种方式复制/移动每个基本或非静态数据成员   适合其类型:
  [...]
   - 否则,基地或成员是   用相应的基数或x的成员直接初始化。

     

虚拟   基类子对象只能初始化一次   隐式定义的复制/移动构造函数(见12.6.2)。

无论如何,因为C是派生程度最高的类,所以C(而不是B}的责任是复制 - 构建虚拟基地A

答案 3 :(得分:10)

考虑一个钻石继承,您可以将C对象传递给B1B2 ctors:

class A { public: int a };

class B1: virtual public A {};
class B2: virtual public A {};

class C: public B1, public B2 {
public:
    C(const C &from): B1(from), B2(from) {}
};

(见http://coliru.stacked-crooked.com/a/b81fad6cf00c664a)。

哪一个应该初始化a成员?第一个,后者,两个(按顺序)?如果B1B2 cctors以不同方式初始化a怎么办?

这就是需要显式调用A cctor的原因,否则A类的成员将被默认构造。

我真正觉得有趣的是,默认编译器设法复制C成员的a cctor,但这是另一个问题。