部分建筑&在层次结构

时间:2016-06-08 15:30:01

标签: c++ inheritance c++14

EDIT1:为了修复UB而编辑的问题在Yakk的答案中指出(这是关于原始问题的有效答案)。

请考虑以下代码:

class C
{
protected:
    C(bool) : c(0)  { s = new char[10]; /* init C members... */ }
    void cleanup()  { delete[s]; /* cleanup C members... */ }   //EDIT1
    C()             { /* do nothing, keep C members unchanged */ }
    // EDIT1: removed dtor: ~C()    { /* do nothing, keep C members unchanged */ }
    // EDIT1: implicitly defined default (trivial) dtor
    int   c;
    char* s;
};

class Child1 : public C
{
public:
    Child1(bool) : C(true)  { }
    void cleanup()          { C::cleanup(); }   //EDIT1
    Child1()                { c ++; }
    // EDIT1: removed dtor: ~Child1()   { }
    // EDIT1: implicitly defined default (trivial) dtor
};

class Child2 : public C
{
public:
    Child2()                { c --; }
    void cleanup()          { C::cleanup(); }   //EDIT1
    // EDIT1: removed dtor: ~Child2()   { }
    // EDIT1: implicitly defined default (trivial) dtor
};

int main()
{
    char storage[sizeof(Child1)];           // (0) storage for any C child instance
    C* child = new(&storage) Child1(true);  // (1) create in-place Child1 instance and initialize C members
    //EDIT1: removed: static_cast<Child1*>(child)->~Child1(); // (2) destroy Child1 instance, keeping C members unchanged
    child = new(&storage) Child2;           // (3) create in-place Child2 instance, keeping C members unchanged, overwritting Child1 members
    //EDIT1: removed: static_cast<Child2*>(child)->~Child2(); // (4) destroy Child2 instance, keeping C members unchanged
    child = new(&storage) Child1(true); // (5) create in-place Child1 instance, keeping C members unchanged, overwritting Child2 members
    //EDIT1: removed: static_cast<Child1*>(child)->~Child1(); // (6) destroy Child1 instance, keeping C members unchanged
    child->cleanup();                       // (7) cleanup Child1 & C members [EDIT1]
    return 0;
}
  • 在第(1)行,Child1实例是使用非默认ctor Child1(bool)创建的“就地”实例。这导致通过非默认ctor C初始化父类C(bool)成员。
  • 在第(2)行,Child1实例被销毁。这将调用父类C的dtor,它是自动实现为空的,以保持C成员不变。 [EDIT1]
  • 在第(3)行,Child2实例是使用Child2的默认ctor创建的“就地”实例。这个覆盖了Child1实例 [EDIT1]并调用了父类C的default-ctor,它是自愿实现为空的,以保持C成员不变。

在此步骤中,Child2实例已能够访问父类C受保护的成员,保持不变尽管Child1实例已被销毁在第(3)行执行的重写操作中。 [EDIT1]

上面描述的模式允许我实现我的主要目标:创建并销毁类C的任何子项的 [EDIT1]实例,保持C成员不变。此外,使用非默认的ctor,我可以初始化C成员(例如第(1)行)。

然而,这种模式有几个缺点:

  • class C成员不能是const或reference,并且必须具有普通的默认ctor和dtor。 (同样的规则适用于任何子成员。)
  • 清理析构函数不能轻易实现(如果C ++支持非默认的dtor,它可能与初始化非默认ctor C(bool)的方式相同,但遗憾的是C ++没有。
  • class C成员不能是const或reference。 [EDIT1]
  • C,类C的父级和类C成员必须将明确定义的默认ctor实现为空(即类似平凡)[EDIT1]
  • 班级C必须有一个微不足道的dtor。 [EDIT1]

我的问题

  • 是上面描述的模式Defined Behavior?[EDIT1]
  • 有没有其他方法可以实现相同的目标( [in-place] [EDIT1] create 和destroy [EDIT1]子类实例的父类{ {1}},保持父类C成员不变)没有上面列出的(特别是第一个) [EDIT1]的缺点?

理想情况下,如果我有办法阻止在子构建/销毁过程中调用父类C ctor和dtor,那么这将是完美的。 [EDIT1] < / p>

注意1:在实际应用程序中,C类可能非常大,C个孩子的构造 / destruction [EDIT1]必须密集发生;就地构造旨在优化此类操作的性能。

注2 [EDIT1]:类C中的普通dtor和子节点被要求在错误调用析构函数时防止未定义的行为;根据C ++标准的§3.8/ 1,在调用析构函数时,具有普通dtor的对象的生命周期不会结束。

2 个答案:

答案 0 :(得分:1)

您正在做的是未定义的行为。

要拥有一个格式良好的程序,您只能通过其正确的类型销毁对象。销毁后,您只能将其存储作为未初始化的缓冲区访问。重新创建时,不能保证变量的状态,也绝对不能保证它们共享以前的状态。

如果您需要这样的行为,您可以实现手动继承方案,例如C程序员在需要OO-heiarchy时使用的方法。

这允许状态数据独立于数据的OO标识进行存储,并允许您动态更改对象的OO标识。

这是一个玩具示例:

struct Base_vtable {
  void(*display)(Base const*);
};
struct Base {
  static init_vtable(Base_vtable* vtable) {
    vtable->display = display_raw;
  }
  static Base_vtable make_vtable() {
    Base_vtable vtable;
    init_vtable(&vtable);
    return vtable;
  }
  static Base_vtable const* get_vtable() {
    static const auto vtable = make_vtable();
    return &vtable;
  }
  Base_vtable const* vtable_data = nullptr;
  Base_vtable const* vtable() const { return vtable_data; }
  std::array<char, 1000*1000> big_buffer;
  std::string name;
  static void display_raw(Base const* self) {
    std::cout << self->name;
  }
  void display() {
    vtable()->display(this);
  }
  static void ctor(Base* self) {
    self->vtable_data = get_vtable();
  }
  static void dtor(Base* self) {
  }
};
struct Derived_vtable:Base_vtable {
  int(*sum)(Derived const*);
};

struct Derived:Base {
  Derived_vtable const* vtable() {
    return static_cast<Derived_vtable const*>(vtable_data);
  }
  static void init_vtable(Derived_vtable* vtable) {
    vtable->print = display_raw;
    vtable->sum = sum_raw;
  }
  static Derived_vtable make_vtable() {
    Derived_vtable d;
    init_vtable(&d);
    return d;
  }
  static Derived_vtable const* get_vtable() {
    static const Derived_vtable vtable = make_vtable();
    return &vtable;
  }
  static int sum_raw(Derived const* self) {
    int r = 0;
    for (auto&& c:big_buffer)
      r+=c;
    return r;
  }
  static void display_raw(Derived const* self) {
    std::cout << "Derived: ";
    Base::display_raw(self);
  }
  int sum() const {
    return vtable()->sum(this);
  }
  static void ctor(Derived* self) {
    Base::ctor(self);
    self->vtable_data = get_vtable();
  }
  static void dtor(Derived* self) {
    Base::dtor(self);
  }
};

当您想要使用默认的C ++ OO系统时,这非常类似于C ++为您做的 。除了现在我们有细粒度的控制,我们可以改变我们各种各样的ctors。

我可以将我的状态与我的虚拟类型分开,允许Derived拥有多个不同的vtable,并在我需要时更改其行为。这些国家之间的过渡可以做我想做的任何事情。

您的解决方案的问题在于允许编译器将其销毁对象的状态用于其选择的任何目的 - 它可以将其用作交换寄存器的空间。它可以假设存储在被破坏的结构中的对象是悬空指针,证明指针是空的或指向该存储,确定如果不为空则我们将取消引用它,并且如果解除引用的行为是UB,然后正确地优化您的代码知道指针必须为空且不检查它,从而消除了死代码分支。

一旦您深入研究未定义的行为,您将被迫在将来的每次迭代编译器中维护您的代码,这可能通过在标准下完全合法的事情来破坏您的代码。除非您正在编写丢弃代码,否则这是一个非常繁重的负载。

答案 1 :(得分:1)

在详尽阅读标准后,我可以回答我自己问题的第一部分。

正如Yakk所提到的,我提出的第一个方案(原始问题)是UB,因为调用对象的析构函数会终止它的生命周期,除非析构函数是微不足道的。 §3.8/ 1标准规定:

  

类型T的对象的生命周期在以下时间结束:

     

- 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或

     

- 重用或释放对象占用的存储空间。

在我提出的更新方案(EDIT1)中,根本没有调用对象的析构函数,而是用新的对象覆盖对象。我虽然这可以摆脱UB,但是同样的§3.8/ 1标准清楚地说明了一个对象的生命周期在它的析构函数被称为或者它占用的存储被重用时结束,正是在覆盖中完成的工作。 (具体而言,这可以使用指针UB。)

然后,我提出的更新方案是UB作为第一种方案。

关于我提问的第二部分,Yakk提供了一个有效的解决方案。