为什么 C++ 的创建者决定使用构造函数初始值设定项列表来初始化基类?
Why did the creator of C++ decide to use constructor initializer list to initialize base classes?
为什么C++的创建者决定使用构造函数初始值设定项列表来初始化基类?为什么他没有选择使用语法,如以下代码中的第二个注释行?
class A{
public:
A() { }
};
class B : A{
public:
B() : A() { } // Why decide to use this one, using constructor initializer, to initialize base class?
B() { A(); } // Why not choose this one? It's easy for me to understand if it was possible.
};
int main(int argc, char *argv[]){
/* do nothing */
}
使用初始值设定项列表的优点是,在构造函数的主体中有一个完全初始化的对象。
class A {
public:
A(const int val) : val(val) { }
private:
const int val;
};
class B : A{
public:
B() : A(123) {
// here A and B are already initialized
}
};
因此,您可以保证所有成员(甚至是父成员的成员)都未处于未定义状态。
编辑
在问题的示例中,没有必要在初始值设定项列表中调用基类,这是自动完成的。所以我稍微修改了这个例子。
我可以看到当前C++规则的一些可能的替代方案,即类类型的基类和数据成员在类类型构造函数的 mem 初始化器列表中初始化。它们都有一系列问题,我将在下面讨论。
但首先,请注意,您不能简单地编写这样的派生构造函数:
struct Derived : Base
{
Derived()
{
Base(42);
}
};
并期望行Base(42);
调用基类构造函数。在C++的其他任何地方,这样的语句都会创建一个使用42
初始化的Base
类型的临时对象。在构造函数中(或仅在其第一行内?)更改其含义将是语法的噩梦。
这意味着需要为此引入新的语法。在本答案的其余部分,我将为此使用假设的结构__super<Base>
。
现在讨论更接近所需语法的可能方法,并提出它们的问题。
备选案文A
基类在构造函数主体中初始化,而数据成员仍在 mem 初始化器列表中初始化。(这是最接近您问题的字母)。
这将有一个直接的问题,即东西以与编写的顺序不同的顺序执行。出于很好的理由,C++中的规则是基类子对象在派生类的任何数据成员之前初始化。想象一下这个用例:
struct Base
{
int m;
Base(int a) : m(a) {}
};
struct Derived
{
int x;
Derived() :
x(42)
{
__super<Base>(x);
}
};
像这样写,你可以很容易地假设x
首先初始化,然后Base
会用42
初始化。但是,情况并非如此,相反,阅读x
将是不确定的。
备选案文B
Mem初始化器列表被完全删除,基类使用__super
初始化,数据成员被简单地在构造函数体中分配(几乎是Java的方式)。
这在C++中不起作用,因为初始化和赋值是根本不同的概念。有些类型中两个操作执行截然不同的操作(例如引用),以及根本无法分配的类型(例如std::mutex
)。
这种方法将如何处理这样的坐姿?
struct Base
{
int m;
Base(int a) : { m = a; }
};
struct Derived : Base
{
double &r;
Derived(int x, double *pd)
{
__super<Base>(x); // This one's OK
r = *pd; // PROBLEM
}
};
考虑标记为// PROBLEM
的行。要么它意味着它通常在C++中做什么(在这种情况下,它将double
分配给未初始化的引用r
引用的"随机位置"),要么我们在构造函数中更改其语义(或仅在构造函数的初始部分中?)进行初始化而不是赋值。前者给了我们一个有缺陷的程序,而后者引入了完全混乱的语法和不可读的代码。
备选案文C
与上面的 B 类似,但引入了用于在构造函数体中初始化数据成员的特殊语法(就像我们对__super
所做的那样)。像__init_mem
:
struct Base
{
int m;
Base(int a) : { __init_mem(m, a); }
};
struct Derived : Base
{
double &r;
Derived(int x, double *pd)
{
__super<Base>(x);
__init_mem(r, *pd);
}
};
现在的问题是,我们取得了什么成就?以前,我们有mem初始化器列表,这是一种用于初始化基和成员的特殊语法。它的优点是,在构造函数主体开始之前,它首先明确了这些事情的发生。现在,我们有一个特殊的语法来初始化基和成员,我们需要强制程序员将其放在构造函数的开头。
请注意,Java 可以摆脱没有 mem 初始化器列表的原因,这些原因不适用于C++:
- 创建对象的语法在 Java 中始终
new Type(args)
,而Type(args)
可以在C++中用于按值构造对象。 - Java 只使用指针,其中初始化和赋值是等效的。对于许多C++类型,操作是不同的。
- Java 类只能有一个基类,因此仅使用
super
个基类就足够了。C++需要区分你指的是哪个基类。
B() : A() { }
这将以用户定义的方式初始化基类。
B() { A(); }
这不会以用户定义的方式初始化基类。 这将在构造函数内部创建一个对象,即B(){}
我认为与第二次初始化相比,第一次初始化具有更好的可读性,您还可以推断出类层次结构。
- 为什么我需要在成员发起器列表中重复基类的模板参数?
- 在初始化列表之外手动调用基类的构造函数
- 虚拟基类的派生类列表
- 为什么 C++ 的创建者决定使用构造函数初始值设定项列表来初始化基类?
- 子类对象列表重新解释为基类对象列表?(C++11).
- 将initalizer列表与从空基类继承的结构一起使用
- 使用不包括基类的模板从C++中的列表中查找特定类型
- 流运算符和多态基类列表
- 为什么在派生最多的类的初始值设定项列表中显式调用虚拟基类构造函数的规则,而较老的祖先已经拥有它?
- 从其基类列表中调用重写的函数
- 是否可以在基类初始化器列表中传递成员对象
- 初始值设定项列表:基类和成员函数中的构造函数
- 自动将派生类的指针列表转换为基类的指针列表
- 如何制作一个既可以包含基类又可以包含派生类的列表
- 在模板基类列表中使用类本地类型别名
- 当后代需要不同的参数列表时,我应该如何在基类中定义抽象方法
- 基类在派生类构造函数初始化列表时没有默认构造函数
- 方法,该方法调用所有基类的同名方法(如果存在),并将返回值保存到列表中
- 从基类模板化列表中实例化子类对象
- STL 容器列表、deque、vector 等的基类是什么?