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

Why does using a virtual base class change the behavior of the copy constructor

本文关键字:构造函数 复制 虚拟 基类 为什么      更新时间:2023-10-16

在下面的程序中,当 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-copyn");
    T t2(t1);
    printf("post-copyn");
    printf("t1.a=%dn", t1.a);
    printf("t2.a=%dn", 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

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

虚拟继承是一个有趣的野兽,因为复制结构不像正常情况下那样被"继承"。您的A基是默认构造的,因为您没有显式复制构造它:

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

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

换句话说,示例中的类层次结构最终是,在某种程度上:

class A {
};
class B {
};
class C : public B, public A {
};

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

因此,你定义了C的复制构造函数,它复制构造B,但是由于A不再是B的子类,所以没有任何复制构造A,因此你看到的行为。

请注意,我刚才所说的所有内容仅适用于C类。正如您所期望的那样,B类本身派生自A。只是当你用虚拟超类声明一个类的其他子类时,所有虚拟超类都会"浮动"到新定义的子类中。

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

非委托构造函数中,初始化在 以下顺序:
— 首先,并且仅对于派生最多的类 (1.8) 的构造函数,虚基类按以下顺序初始化 它们出现在定向的深度优先从左到右遍历上 基类的非循环图,其中"从左到右"是 派生类中基类的外观 基本说明符列表。
— [直接基类等...]

这基本上说明了它 - 大多数派生类负责以它定义它的任何方式进行初始化(在OP中:它没有,这会导致默认初始化)。标准中的后续示例具有与此处 OP 中类似的场景,只是对 ctor 有一个 int 参数;仅调用虚拟基的默认 CTOR,因为在最派生的类中没有为虚拟基提供显式的"内存初始值设定项"。

感兴趣的是,虽然在这里也不直接适用,但也是 12.6.2/7:

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

(我觉得这很难。语言基本上是说"我不在乎你编码了什么,我会忽略它。没有那么多地方可以做到这一点,违反假设。非派生类的构造函数将B() 。这句话在这里不直接适用,因为B中没有显式构造函数,所以也没有mem初始化器。但是,尽管我在标准中找不到措辞,但必须假设(并且是一致的)相同的规则适用于生成的复制构造函数。

为了完整起见,Stroustrup在"C++编程语言"(4.ed,21.2.5.1)中谈到了一个最派生的D类,在某处有一个虚拟基数V:

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

这正是Sam Varshavchik在之前的一篇文章中所说的。

Stroustrup接着讨论,从D派生一个DD类使得有必要将V的初始化移动到DD,这"可能很麻烦。这应该鼓励我们不要过度使用虚拟基类。

我发现基类保持未初始化(好吧,更准确地说:默认初始化)相当晦涩和危险,除非大多数派生类显式执行某些操作。

派生最多的类的作者必须深入研究继承层次结构 他/她可能不感兴趣或文档,也不能依赖例如他/她用来做正确事情的库(库不能)。

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

默认复制 ctor 确实初始化虚拟基础的有趣事实在 12.8/15 中规定:

每个基本或非静态数据成员都按以下方式复制/移动 适合其类型:
[...]
— 否则,基数或成员是 使用 X 的相应基或成员直接初始化。

虚拟 基类子对象只能由 隐式定义的复制/移动构造函数(请参阅 12.6.2)。

无论如何,因为C是最派生的类,所以复制构造虚拟基AC(而不是B)的责任。

考虑一个菱形继承,您将要从中复制的C对象传递给B1B2 ctor:

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 cctor 以不同的方式初始化a,该怎么办?

这就是为什么您需要显式调用 A cctor,否则 A 类的成员将默认构造。

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