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

ctn*_*ctn 19 c++

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

#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>();
}
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

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

为什么不在a第一种情况下复制(test<C>()B实际上来自A?

Lig*_*ica 20

虚拟继承是一个有趣的野兽,因为复制结构不像通常那样"继承".您的A基础是默认构造的,因为您没有明确地复制它:

class C : public B {
public:
    C() {}
    C(const C &from) : A(from), B(from) {}
};
Run Code Online (Sandbox Code Playgroud)


Sam*_*hik 15

理解虚拟继承的最好方法是理解虚拟继承的类总是由派生类最多的子类化.

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

class A {
};

class B {
};

class C : public B, public A {
};
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,我刚才所说的所有内容仅适用于该C课程.这个B类本身就是A你所期望的.只是当您使用虚拟超类声明类的其他子类时,所有虚拟超类都"浮动"到新定义的子类.


Pet*_*ica 12

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

在非委托构造函数中,初始化按以下顺序进行:
- 首先,仅对于派生程度最高的类(1.8)的构造函数,虚拟基类按它们在深度优先左侧出现的顺序进行初始化.正确遍历基类的有向非循环图,其中"从左到右"是派生类base-specifier-list中基类出现的顺序.
- [直接基类等...]

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

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

一个mem-initializer [ A()在一个可能的B(): A() {}.-pas]其中mem-initializer-id表示在执行不是最派生类的任何类的构造函数期间忽略虚拟基类.

(我发现这很难.语言基本上说"我不在乎你编码的是什么,我会忽略它."没有那么多的地方可以做到这一点,违反了as-if.)那个构造函数一个不是最派生的类B().这里的句子没有直接适用,因为没有明确的构造函数B,所以也没有mem-initializer.但是,虽然我无法在标准中找到相应的措辞,但必须假设(并且它是一致的)相同的规则适用于生成的复制构造函数.

为了完整起见,Stroustrup在"The C++ Programming Language"(4.ed,21.2.5.1)中说到了一个带有虚拟基础V的最衍生的D类:

事实上,V没有明确地被提及作为D的基础是无关紧要的.对虚拟基础的了解以及初始化它的义务会"冒泡"到最派生的类.虚拟基础始终被视为其派生类最直接的基础.

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

然后Stroustrup继续讨论从D派生DD类使得有必要将V的初始化移到DD,这"可能会令人讨厌.这应该鼓励我们不要过度使用虚拟基类."

我发现基类保持未初始化(嗯,更准确地说:默认初始化)是非常模糊和危险的,除非最派生的类明确地做了某些事情.

派生程度最高的类的作者必须深入研究一个他/她可能不感兴趣或不需要文档的继承层次结构,并且不能依赖于他/她用来做正确事情的库(库不能).

我也不确定我同意其他帖子中给出的理由("各个中间类应该执行初始化?").该标准有一个明确的初始化顺序概念("深度优先从左到右遍历").它能否强制要求从基数中实际继承的第一个类执行初始化?

有趣的事实是,默认的构造函数复制初始化虚拟基地12.8/15规定:

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

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

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


Dar*_*ioP 10

考虑一个钻石继承,你传递C对象从中复制到两者B1B2ctors:

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) {}
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

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