为什么 C++ 不允许派生 class 在初始化列表中使用基 class 成员?

Why C++ doesn't allow derived class to use base class member in initialization list?

我有以下程序:

#include<iostream> 
using namespace std; 
struct Base01{ 
    int m; 
    Base01():m(2){} 
    void p(){cout<<m<<endl;} 
}; 
struct Derived01:public Base01{ 
    Derived01():m(3){} 
}; 
struct Derived02:virtual public Base01{ 
    Derived01():m(4){} 
}; 
struct my: Derived01,Derived02{ 
    my():m(5){} 
}; 
int main(){ 
    return 0; 
} 

两个gcc/clang都报告编译错误。

我只是想知道这里的语言设计考虑是什么,为什么derived class 只能调用初始化列表中的base class ctor,而不能直接使用base class 成员?

你在构造函数初始化列表中所做的是初始化。这是必须在对象的生命周期内只做一次的事情。在一般情况下,这就是启动对象 lifetime 的原因。

Base class 的构造函数(在派生的 class 的构造函数被激活之前完成工作)已经初始化了 base class 的所有直接子对象。它已经开始了他们的生命。如果您尝试从派生 class 的构造函数中获取并初始化基 class 的直接子对象,那显然是同一对象的第二次初始化。这在 C++ 中是完全不能接受的。语言设计一般不允许你对某个东西进行二次初始化。

在你的例子中,有问题的子对象具有基本类型 int,因此很难看出这种 "re-initialization" 的危害。但是考虑一些不那么微不足道的东西,比如 std::string 对象。你如何建议派生 class 应该 "undo and redo" 已经由基 class 执行的初始化?虽然形式上可以正确地做到这一点,但构造函数初始化列表并不是为了这个目的。

在一般情况下,做这样的事情需要一种语言特性,允许用户按照 "please, leave this subobject of yours uninitialized, I will reach-in and initialize it later from the derived class" 的方式告诉基础 class 的构造函数。然而,C++ 并没有为用户提供这样的能力。虚拟基 class 初始化中存在一个模糊相似的功能,但它有一个非常具体(和不同)的目的。

在 C++ 中执行此操作的正确方法是将该值传递给基础 class 构造函数。您的 Base01 class 需要一个额外的构造函数来获取所需的 m 值。像这样:

struct Base01{ 
    int m; 
    Base01():m(2){}

    // Added this:
    Base01(int mVal) : m(mVal) {} 

    void p(){cout<<m<<endl;} 
}; 

struct Derived01:public Base01{ 
    Derived01() : Base01(3) {}   // Calling base constructor rather than
                                 // initializing base member
};

struct Derived02:virtual public Base01{ 
    Derived01() : Base01(4){}    // Same here
};

struct my: Derived01,Derived02{ 
    my(): Base01(5){}            // And here.
}; 

正如 AnT 所说,你不能初始化两次——但你可以设置它,以便首先按照你想要的方式初始化。

您当然可以使用 ctor-initializer 列表中的基 class 成员:

struct Base
{
    int x;
    Base(int x) : x(x) {}
};

struct Derived
{
    int y;
    Derived() : Base(7), y(x) {}
}

这里,基成员x出现在派生成员y的初始化器中;它的值将被使用。

A​​nT 很好地解释了为什么 ctor-initializer 列表不能用于(重新)初始化基础子对象的成员。

这里的基本语言设计考虑是关注点分离(避免使基础 class 依赖于其派生的 classes),并且基础 class 负责初始化它自己的成员(以及它拥有的任何基础)。

一个相关的考虑是基 class 的成员不存在 - 就派生的 class 构造函数而言 - 在基 class 构造函数完成之前。如果派生 class 的初始化列表能够进入并初始化基 class 成员,则可能有两种结果

  • 如果基础 class 构造函数没有被调用,当派生 class 试图初始化它们时,它的成员将不存在。
  • 如果基 class 构造函数已被调用,则该成员已被初始化。初始化(与重新初始化的赋值不同)在对象的生命周期中发生一次,因此再次初始化它没有意义。

这两种可能性在实践中都没有实际意义,除非基 class 设计不当(例如,其构造函数未正确初始化其成员)。需要什么样的机器才能使它们有意义(例如,改变层次结构中 base classes 的构造顺序,取决于派生 class 试图初始化的成员)会使编译器机器更加复杂(例如,能够控制和跟踪基础 class 成员的构造顺序,以防派生 class 应该选择进入),并且还意味着 [=24] 的构造顺序=]es 将取决于派生的 classs)。这将引入基本 class 行为(初始化方式)对派生 classes 的依赖。

更简单的方法是让基础 class 提供一个正确初始化相关成员的构造函数,并让派生 class 构造函数调用该基础 class 构造函数它的初始化列表。所有上述(假设的)考虑都会消失。