为什么使用虚拟基类会更改复制构造函数的行为
Why does using a virtual base class change the behavior of the copy constructor
在下面的程序中,当 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
。
为什么在第一种情况下没有复制a
(test<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
是最派生的类,所以复制构造虚拟基A
是C
(而不是B
)的责任。
考虑一个菱形继承,您将要从中复制的C
对象传递给B1
和B2
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
成员?第一个,后者,两者都(按什么顺序)?如果 B1
和 B2
cctor 以不同的方式初始化a
,该怎么办?
这就是为什么您需要显式调用 A
cctor,否则 A
类的成员将默认构造。
我真正觉得有趣的是,默认C
cctor编译器设法复制a
成员,但这是另一个问题。
- C++17复制构造函数,在std::unordereded_map上进行深度复制
- 为什么在C++中使用私有复制构造函数与删除复制构造函数
- 当从函数参数中的临时值调用复制构造函数时
- 如果有一个模板构造函数只有一个泛型参数,为什么我必须有一个复制构造函数
- 使用复制构造函数复制双精度数组
- C 无可行的构造函数复制类型的变量
- 没有可行的构造函数复制类型 'MyString' 的数组元素
- 编译时,复制构造函数/复制分配和正常功能调用优化之间是否存在任何区别
- 如何最小化调用列表构造函数(复制构造函数)的次数?
- C 11矢量构造函数复制与范围
- 我定义了一个非复制构造函数;复制构造函数还会被隐式定义吗
- 可以将构造函数复制为转换运算符
- 将基类指针的构造函数复制到子类
- C++树类:构造函数/复制/内存泄漏
- 如何制作这个在模板构造函数复制中使用类型定义的类型的模板
- 将构造函数复制为模板化的成员函数
- 绕过私有复制构造函数/复制赋值C++
- C++通过构造函数复制对象
- 复制构造函数 - 复制C++中的对象
- 将带unique_ptr的类的构造函数复制到作为成员的抽象类