为什么 C++ 的创建者决定使用构造函数初始值设定项列表来初始化基类?

Why did the creator of C++ decide to use constructor initializer list to initialize base classes?

本文关键字:列表 基类 初始化 创建者 C++ 决定 构造函数 为什么      更新时间:2023-10-16

为什么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(){}

我认为与第二次初始化相比,第一次初始化具有更好的可读性,您还可以推断出类层次结构。