为什么 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
的初始化器中;它的值将被使用。
AnT 很好地解释了为什么 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 构造函数它的初始化列表。所有上述(假设的)考虑都会消失。
我有以下程序:
#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
的初始化器中;它的值将被使用。
AnT 很好地解释了为什么 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 构造函数它的初始化列表。所有上述(假设的)考虑都会消失。