层次结构中子类对象的部分构造和销毁

Partial construction & destruction of an object of a child class in hierarchy

本文关键字:子类 对象 层次结构      更新时间:2023-10-16

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实例是使用非默认 ctorChild1(bool)"就地"创建的。这导致通过非默认 ctorC(bool)初始化父类C成员。
  • 在第 (2) 行,Child1实例被销毁。这将调用父类的 dtorC,它自愿实现为空,以保持C成员不变。[编辑1]
  • 在第 (3) 行,Child2实例是使用默认 ctorChild2"就地"创建的。这将覆盖 Child1 实例[EDIT1] 并调用父类C的默认 ctor,该默认为空以保持C成员不变。

在此步骤中,Child2实例已经能够访问父类C受保护的成员,尽管Child1实例在第 (3) 行执行的覆盖操作中

已被销毁,但保持不变。[编辑1]

上面描述的模式允许我实现我的主要目标:创建和

销毁C的任何子级的[EDIT1]实例,保持C成员不变。此外,使用非默认 ctor,我有一种方法可以初始化C成员(例如在第 (1) 行)。

但是,此模式有几个缺点:

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

我的问题

  • 上面描述的模式是定义的行为吗?[编辑1]
  • 有没有其他方法可以实现相同的目标([
  • 就地][EDIT1]创建和销毁父类C的[EDIT1]子类实例,保持父类C成员不变)而没有上面列出的缺点(尤其是第一个)[EDIT1]?

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

注1:在实际应用中,C类可能相当大,C子项的构造

/销毁[EDIT1]必须密集进行;就地构造旨在优化此类操作的性能。

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

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

要获得格式正确的程序,只能按其正确的类型销毁对象。 销毁后,只能将其存储作为未初始化的缓冲区进行访问。 重新创建时,不能保证变量的状态,也不能保证它们共享以前的状态。

如果需要此类行为,可以实现手动继承方案,例如 C 程序员在需要 OO 层次结构时使用的方案。

这允许独立于数据的 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++想要使用默认C++OO系统时为您执行的操作非常相似。 除了现在我们有细粒度的控制,我们可以改变我们的各种 ctor 做什么。

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

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

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

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

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

类型T对象的生存期在以下情况下结束:

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

— 对象占用的存储被重复使用或释放。

在我提出的更新方案 (EDIT1) 中,根本不调用对象的析构函数,而是用新对象覆盖对象。我虽然这会摆脱 UB,但是标准的相同 §3.8/1 明确指出,当调用对象的析构函数或重用它占用的存储时,对象的生存期结束,这正是覆盖所做的。(具体而言,这使用了指针 UB。

然后,我提出的更新方案与第一个方案一样是UB。

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