0

我有以下层次结构:

  Base_class
      |
 Traits_class
      |
Concrete_class

现在问题是数据包含在Base_class(它需要在那里,因为Traits_class必须可以访问它。Traits_class是一个类模板,根据传递的模板参数具有不同的功能(所以我使用部分模板特化来处理不同的classes)。最后,在最低级别,Concrete_class也是一个类模板。我Concrete_class只创建实例。

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

感谢您的洞察力!

编辑

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

 Traits_class
      |
Concrete_class

我还将所有数据Concrete_class移至Traits_class. 但是发生了一些奇怪的事情,因为我无法访问Traits_class. Traits_class我的意思是,我确实访问过它,但似乎我正在访问幽灵数据,因为我在其中初始化了成员Traits_class(甚至在Traits_class构造函数中打印),但随后该类为空。所以我真的不明白发生了什么(我正在将 Traits_class const_casting 到 Concrete_class 来执行此操作)。

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

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

4

1 回答 1

3

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

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

如果必须从 Concrete_class 初始化所有数据成员,我建议您在 Base_class 中提供一个受保护的构造函数,该构造函数按值获取所有数据成员并将它们移动到自己的数据成员中,并在 Traits_class 中提供完美转发构造函数(或继承基类的构造函数,如果您使用的是具有此支持的编译器)。这允许具体类指定初始化数据成员的值,但允许基类进行实际初始化。它还允许 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 提到的 Curiously Recurring Template Pattern。

== 回复在原始问题中编辑 ==

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

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

#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
*/
于 2013-04-05T17:44:01.223 回答