C++中的存储重用

Storage reuse in C++

本文关键字:存储 C++      更新时间:2023-10-16

我一直在努力了解C++中的存储重用。假设我们有一个对象a了一个非平凡的析构函数,其存储通过放置新表达式重用:

struct A {
    ~A() { std::cout << "~A()" << std::endl; }
};    
struct B: A {};
A* a = new A;     // lifetime of *a begins
A* b = new(a) B;  // storage reuse, lifetime of *b begins

[basic.life/8] 指定:

如果在对象的生存期结束后,在重新使用或释放对象所占用的存储

之前,在原始对象占用的存储位置创建一个新对象、指向原始对象的指针、引用原始对象的引用或原始对象的名称,并且, 一旦新对象的生存期开始,如果原始对象可由新对象透明地替换(见下文(,则可用于操作新对象。

由于在我的示例中,当我们重用*a占用的存储空间时,的生命周期尚未结束,因此我们无法应用该规则。那么什么规则描述了我的情况中的行为呢?

§3.8 [basic.life]/p1 和 4 中规定了适用的规则:

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

  • 如果 T 是具有非平凡析构函数 (12.4( 的类类型,则析构函数调用将启动,或者
  • 对象占用的存储被重用或释放。

4 程序可以通过重用存储来结束任何对象的生存期 对象占据的或通过显式调用析构函数 具有非平凡析构函数的类类型的对象。对于对象 具有非平凡析构函数的类类型,该程序不是 需要在存储之前显式调用析构函数 对象被重用或释放;但是,如果没有 显式调用析构函数,或者如果删除表达式 (5.3.5( 是 不用于释放存储,析构函数不得 隐式调用和任何依赖于副作用的程序 析构函数生成的行为未定义。

因此,A *b = new (a) B;重用在上一个语句中创建的A对象的存储,这是定义良好的行为,前提是sizeof(A) >= sizeof(B) *。该A对象的生存期已因重用其存储而结束。 A 的析构函数不是为该对象调用的,如果您的程序依赖于该析构函数产生的副作用,则它具有未定义的行为。

您引用的段落 §3.8 [basic.life]/p7 规定了何时可以重用指向原始对象的指针/引用。由于此代码不满足该段落中列出的条件,因此您只能以 §3.8 [basic.life]/p5-6 允许的有限方式或未定义的行为结果(省略示例和脚注(使用 a

5 在对象的生存期开始之前,但在存储之后 对象将占用的对象已分配,或者在生存期后 对象已经结束和之前存储的对象 被占用被重用或释放,任何引用存储的指针 可以使用对象将要或曾经所在的位置,但只能使用 以有限的方式。有关正在建造或销毁的对象,请参阅 12.7. 否则,这样的指针引用分配的存储空间 (3.7.4.2(,并且使用指针就像指针是 void* 类型一样,是 定义明确。这样的指针可能会被取消引用,但结果 左值只能以有限的方式使用,如下所述。这 在以下情况下,程序具有未定义的行为:

  • 对象将是或曾经是具有非平凡析构函数的类类型,指针用作 删除表达式
  • 指针用于访问非静态数据成员或调用对象的非静态成员函数,或
  • 指针隐式转换为 (4.10( 到指向基类类型的指针,或者
  • 指针用作static_cast (5.2.9( 的操作数(除非转换为 void* 或转换为 void* 和 随后char*unsigned char* (,或
  • 指针用作dynamic_cast的操作数 (5.2.7(。

6 同样,在对象的生存期开始之前,但在对象生存期之后 对象将占用的存储已分配,或者之后 对象的生存期已结束,并且在存储之前 占用的对象被重复使用或释放,任何引用 可以使用原始对象,但只能以有限的方式使用。对于对象 正在建造或销毁中,见12.7。否则,这样的glvalue 指分配的存储 (3.7.4.2(,并使用 不依赖于其值的glvalue是明确定义的。该计划 在以下情况下具有未定义的行为:

  • 左值到右值的转换 (4.1( 应用于这样的 glvalue,
  • glvalue 用于访问非静态数据成员或调用对象的非静态成员函数,或者
  • glvalue 隐式转换为 (4.10( 对基类类型的引用,或者
  • glvalue 用作static_cast (5.2.9( 的操作数,除非最终转换为 cv char&cv unsigned char& ,或
  • glvalue 用作dynamic_cast的操作数 (5.2.7( 或 typeid 的操作数。

* 为了防止 UB 出现sizeof(B) > sizeof(A)的情况,我们可以将A *a = new A;重写为 char c[sizeof(A) + sizeof(B)]; A* a = new (c) A;

这有一些潜在的问题:

  1. 如果 B 大于 A,它将覆盖未分配的字节 - 这是未定义的行为。
  2. a(或b - 您的代码不显示您是delete a还是delete b或两者都不要求 A 析构函数(。如果对于AB析构函数正在执行诸如引用计数,锁,内存释放(包括std::容器,例如std::vectorstd::string(等,则这一点非常重要。

如果在创建b后不再使用 a ,您仍然需要调用 A 析构函数以确保它的生命周期已结束 - 请参阅您引用的部分之后的第三个欺凌中的示例。因此,如果您的目的是避免"昂贵"的析构函数调用,那么您的代码就没有遵守标准第 3.8/7 节中给出的规则。

您还违反了以下要点:

  • 原始对象是 T 类型派生最多的对象 (1.8(,新对象是 T 类型派生最多的对象。

因为A不是派生最多的类型。

总之,"坏了"。即使在它确实有效的情况下(例如更改为 A* a = new B; (,也应该劝阻它,因为它可能导致微妙和困难的错误。

作为附录,为了正确执行此操作,您可以显式调用析构函数。

注意:定位的内存大小为 B,以适应 AB 之间的潜在大小。

注意 2:对于您的 A 类实现,这将不起作用。 ~A()必须虚拟化!!

A *b = new B; //Lifetime of b is starting. It is important that we use `new B` rather than `new A` so as to get the correct size.
b->~B(); //lifetime of b has ended. The memory still remain allocated however.
A *a = new (a) A; //lifetime of a is starting
a->~A(); // lifetime of a has ended
// a is still allocated but in an undefined state
::operator delete(b); // release the memory allocated without calling the destructor. This is different from calling 'delete b'

我相信在基本指针上调用operator delete应该是安全的。如果不是这种情况,请纠正我。

或者,如果将 的内存分配为 char缓冲区,则可以使用 place new 构造AB对象,并安全地调用 delete[] 来释放缓冲区(因为 char 有一个简单的析构函数(:

char* buf = new char[sizeof(B)];
A *a = new (a) A;
a->~();
A *b = new (a) B;
b->~B();
delete[] buf;