【问题标题】:Partial construction & destruction of an object of a child class in hierarchy层次结构中子类对象的部分构造和销毁
【发布时间】:2016-06-08 15:30:01
【问题描述】:

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

考虑以下代码:

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(bool) 初始化父类 C 成员。
  • 在第 (2) 行,Child1 实例被销毁。这将调用父类C的dtor,为了保持C成员不变,它自愿实现为空。 [EDIT1]
  • 在第 (3) 行,Child2 实例是使用默认 ctor Child2“就地”创建的。这覆盖了 Child1 实例[EDIT1] 并调用父类C 的 default-ctor,为了保持C 成员不变,它自愿实现为空。

在这一步,Child2 实例已经能够访问父类 C 受保护的成员,保持不变尽管Child1 实例在执行的覆盖操作中已被销毁第 (3) 行。 [编辑1]

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

但是,这种模式有几个缺点:

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

我的问题:

  • 是上面描述的模式定义的行为?[EDIT1]
  • 是否有任何其他方法可以实现相同的目标([in-place][EDIT1] 创建 并销毁[EDIT1] 父类的子类实例 @ 987654346@,保持父类C成员不变)没有上面列出的缺点(尤其是第一个)[EDIT1]?

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

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

注意2[EDIT1]:类C 和children 中的琐碎dtor 需要在错误调用析构函数的情况下防止未定义行为;根据 C++ 标准的 §3.8/1,当调用析构函数时,具有平凡 dtor 的对象的生命周期不会结束。

【问题讨论】:

    标签: c++ inheritance c++14


    【解决方案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,然后正确优化您的代码知道指针必须为空而不检查它,消除死代码分支。

    一旦您深入研究未定义的行为,您就不得不针对未来编译器的每次迭代维护您的代码,这些迭代可能会通过在标准下完全合法地做一些事情来破坏您的代码。除非您正在编写一次性代码,否则这是一个非常沉重的负担。

    【讨论】:

    • 感谢您的回答以及您在我的方案中指出的 UB。我更新了我的问题(和我的计划)以摆脱 UB。您作为替代解决方案提出的“手动继承方案”看起来很棒,但实施和使用有点棘手;不过,我会更深入地研究它;非常感谢。
    【解决方案2】:

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

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

    T 类型对象的生命周期结束于:

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

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

    在我提出的更新方案(EDIT1)中,对象的析构函数根本没有被调用,而是被一个新的对象覆盖。我虽然这会摆脱 UB,但是标准的相同 §3.8/1 清楚地指出对象的生命周期在其析构函数被调用时结束,或者如果它占用的存储被重用,那正是覆盖所做的。 (具体来说,这利用了 child 指针 UB。)

    那么,我提出的更新方案和第一个方案一样是UB。

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

    【讨论】:

      猜你喜欢
      • 2013-07-28
      • 2016-03-25
      • 1970-01-01
      • 2016-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多