【问题标题】:Why C++ doesn't allow derived class to use base class member in initialization list?为什么 C++ 不允许派生类在初始化列表中使用基类成员?
【发布时间】:2017-12-13 01:22:47
【问题描述】:

我有以下程序:

#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 都报编译错误。

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

【问题讨论】:

    标签: c++ list class initialization derived


    【解决方案1】:

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

    基类的构造函数(在派生类的构造函数属性激活之前完成工作)已经初始化了基类的所有直接子对象。它已经开始了他们的生命。如果您尝试从派生类的构造函数中访问并初始化基类的直接子对象,那显然将是同一对象的第二次初始化。这在 C++ 中是完全不可接受的。语言设计通常不允许您进行第二次初始化。

    在您的情况下,有问题的子对象具有基本类型int,因此很难看到这种“重新初始化”的危害。但是考虑一些不那么琐碎的事情,比如std::string 对象。你如何建议派生类应该“撤消和重做”基类已经执行的初始化?虽然形式上可以正确执行此操作,但构造函数初始化程序列表并非用于此目的。

    在一般情况下,做类似的事情需要一个语言特性,允许用户告诉基类的构造函数“请不要初始化你的这个子对象,我会在稍后从派生类”。但是,C++ 并没有为用户提供这种能力。虚拟基类初始化中存在一个类似的功能,但它服务于一个非常特定(和不同)的目的。

    【讨论】:

    • 排序有点错误——派生构造函数可以控制基子对象构造函数的参数,如果在派生构造函数获得控制之前调用基构造函数,这显然是不可能的.谈论建设责任可能会更好。 (当然,无论哪个先发生,双重初始化仍然是个问题)
    • @BenVoigt 好点。我的意思是说基类是首先构造的,并且只有在派生类构造函数中发生“其他所有事情”之后。但是当然,如​​果语言允许用户在派生构造函数初始化列表中引用基类成员,那么排序规则就会不同(很可能)。不管顺序如何,避免双重初始化的问题仍然存在。
    【解决方案2】:

    在 C++ 中执行此操作的正确方法是将该值传递给基类构造函数。您的Base01 类需要一个额外的构造函数来获取所需的 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 所说,您不能初始化两次——但您可以设置它,以便按照您想要的方式进行初始化。

    【讨论】:

      【解决方案3】:

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

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

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

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

      【讨论】:

        【解决方案4】:

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

        一个相关的考虑是基类的成员不存在 - 就派生类构造函数而言 - 在基类构造函数完成之前。如果派生类的初始化器列表能够访问并初始化基类成员,那么有两种可能的后果

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

        这些可能性在实践中都没有真正意义,除非基类设计不佳(例如,它的构造函数没有正确初始化它的成员)。需要的那种机制才能使它们有意义(例如,改变层次结构中基类的构造顺序,取决于派生类试图初始化的成员)将使编译器机制更加复杂(例如,能够控制和跟踪基类成员的构造顺序,以防派生类选择进入),也意味着类的构造顺序将取决于派生类)。这将引入基类行为(初始化它的方法)对派生类的依赖性。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-11-10
          • 1970-01-01
          • 1970-01-01
          • 2016-02-14
          • 2019-01-25
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多