设计和初始化派生类中的数据

时间:2013-04-05 13:14:52

标签: c++ c++11 constructor initialization

我有以下层次结构:

  Base_class
      |
 Traits_class
      |
Concrete_class

现在的问题是数据包含在Base_class中(它需要存在,因为Traits_class必须能够访问它。Traits_class是一个类模板,不同的功能取决于传递的模板参数(所以我对不同的类使用部分模板特化)。最后,在最低级别,Concrete_class也是一个类模板。我只创建Concrete_class的实例

现在的问题是:我编写了所有构造函数,析构函数,并在Concrete_class中提供了移动语义。这意味着我不调用基础构造函数,但我直接在派生类中初始化状态。 有人可以指出这是否有问题?只有析构函数在Base_class中声明,并声明为受保护。这个设计有明显的缺陷吗?

感谢您的见解!

修改

所以我按照Yakk对CRTP的评论修改了设计,现在我已经

 Traits_class
      |
Concrete_class

我还将所有数据都移到Concrete_class,感谢CRTP,我可以在Traits_class中访问它。虽然发生了一些奇怪的事情,因为我无法在Traits_class的构造函数中访问Traits_class中的数据。我的意思是,我确实访问了它,但似乎我正在访问ghost数据,因为我在Traits_class初始化了成员(甚至在Traits_class构造函数中打印),但之后就是上课是空的。所以我真的不明白发生了什么(我正在将Traits_class const_cast输入Concrete_class来执行此操作)。

最后,我在Traits_class中只编写了静态成员函数来初始化Concrete_class的成员。我想我可以使用受保护的成员函数来做同样的事情(因为我从Traits_class继承),但我相信它是一样的。

如果您有任何进一步的意见,请告诉我。再次感谢您的C ++智慧。

AA

1 个答案:

答案 0 :(得分:3)

你的推理有误。构造函数总是初始化所有基类和非静态成员(从技术上讲,虚拟基础由最派生类型而不是任何其他基础构造函数初始化),因此Base_class实际上将由其编译器生成的默认构造函数初始化(或编译器生成的复制/移动构造函数,如果您正在进行复制或移动),这将使用其默认(或复制/移动)构造函数初始化所有数据成员。您可以稍后在具体类的构造函数中分配这些成员,但此时已经发生了初始化。

由于基类拥有所有数据成员,因此它实际上是在发生复制或移动时初始化所有数据成员的基类。如果你在最派生的类中编写自己的副本或移动构造函数,你需要在初始化列表中调用基类的复制/移动构造函数,否则数据成员将被默认构造,你将被迫事后使用复制/移动分配。这通常效率低下,在某些情况下可能不正确。 (例如,我编写了可以移动构造的类,但由于切片问题而无法移动分配;如果您的Base_class中有这样的类作为数据成员,则无法单独实现移动语义在Concrete_class中。)

如果必须初始化Concrete_class中的所有数据成员,我建议您在Base_class中提供受保护的构造函数,该构造函数按值获取所有数据成员并将它们移动到自己的数据成员中,并在Traits_class中提供完美转发构造函数(或如果您正在使用具有此支持的编译器,则继承base的构造函数。这允许具体类指定初始化数据成员的值,但允许基类进行实际初始化。它还允许Base_class和Traits_class构造函数访问完全初始化的数据成员(否则它们只能访问处于默认初始化状态的数据成员)。

这是一个例子:

struct Base_class {
protected:
    Base_class( string s ) : s_( move(s) ) { }
    ~Base_class() = default;
    // Request copy/move, since we're declaring our own (protected) destructor:
    Base_class(Base_class const &) = default;
    Base_class(Base_class &&) = default;
    Base_class &operator=(Base_class const &) = default;
    Base_class &operator=(Base_class &&) = default;
     
    string s_;
};
 
template<int>
struct Traits_class : Base_class {
protected:
    template<typename... P>
    Traits_class( P &&p )
        : Base_class( forward<P>(p)... )
    { }
};
 
template<int I>
struct Concrete_class : Traits_class<I> {
    Concrete_class( char c )
        : Traits_class<I>( string( I, c ) )
    { }
};

作为旁注,数据不一定必须在Base_class中才能让Traits_class访问它。如果你不介意虚函数调用的开销,并且你不需要在构造函数或析构函数中访问(我假设你没有,因为当前的数据),你可以通过受保护的虚函数提供访问在Concrete_class的构造函数运行之后,成员才有最终值。或者,为了避免虚拟呼叫开销,您可以使用奇怪的重复模板模式作为@Yakk提及。

==编辑原始问题的回复==

基类的构造函数将在派生类的构造函数之前运行,因此如果数据存储在派生类中,它将在基类的构造函数中未初始化(并且已在其析构函数中回收)。您可以将构造函数视为获取基类的实例并将其转换为派生类的实例(通过初始化类的派生部分等),并且作为特殊情况,没有基类的类的构造函数类将“无”(原始存储)转换为类的实例。

因此,当你的traits类构造函数正在运行时,它还不是一个具体的派生类。因此,访问派生类的数据成员或以其他方式将类用作派生类是非法的。该语言甚至强制执行虚拟功能;如果在基类的构造函数或析构函数中调用虚函数,它将调用函数的基本版本。例如:

#include <iostream>
using namespace std;

struct Base {
  Base() { cout << "Base ctor\n"; v(); }
  ~Base() { v(); cout << "Base dtor\n"; }
protected:
  virtual void v() const { cout << "Base::v\n"; }
};

struct Derived : Base {
  Derived() { cout << "Derived ctor\n"; v(); }
  ~Derived() { v(); cout << "Derived dtor\n"; }
protected:
  virtual void v() const { cout << "Derived::v\n"; }
};

int main() { Derived d; }

/* Output:
Base ctor
Base::v
Derived ctor
Derived::v
Derived::v
Derived dtor
Base::v
Base dtor
*/