C++中的存储重用
Storage reuse in C++
我一直在努力了解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;
。
这有一些潜在的问题:
- 如果 B 大于 A,它将覆盖未分配的字节 - 这是未定义的行为。
a
(或b
- 您的代码不显示您是delete a
还是delete b
或两者都不要求 A 析构函数(。如果对于A
或B
析构函数正在执行诸如引用计数,锁,内存释放(包括std::
容器,例如std::vector
或std::string
(等,则这一点非常重要。
如果在创建b
后不再使用 a
,您仍然需要调用 A
析构函数以确保它的生命周期已结束 - 请参阅您引用的部分之后的第三个欺凌中的示例。因此,如果您的目的是避免"昂贵"的析构函数调用,那么您的代码就没有遵守标准第 3.8/7 节中给出的规则。
您还违反了以下要点:
- 原始对象是 T 类型派生最多的对象 (1.8(,新对象是 T 类型派生最多的对象。
因为A
不是派生最多的类型。
总之,"坏了"。即使在它确实有效的情况下(例如更改为 A* a = new B;
(,也应该劝阻它,因为它可能导致微妙和困难的错误。
作为附录,为了正确执行此操作,您可以显式调用析构函数。
注意:定位的内存大小为 B,以适应 A
和 B
之间的潜在大小。
注意 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 构造A
和B
对象,并安全地调用 delete[]
来释放缓冲区(因为 char
有一个简单的析构函数(:
char* buf = new char[sizeof(B)];
A *a = new (a) A;
a->~();
A *b = new (a) B;
b->~B();
delete[] buf;
- 将字符串存储在c++中的稳定内存中
- std::原子加载和存储都需要吗
- C++:将控制台输出存储在宏中更好吗
- 使用QProcess执行命令,并将结果存储在QStringList中
- 访问存储在向量C++中的结构的多态成员
- 如何从存储在std::映射中的std::集中删除元素
- 存储模板类型以强制转换回派生<T>
- 类型总是使用其大小存储在内存中吗
- 当字符串存储在变量中时,如何将字符串转换为wchar_t
- 使用无符号字符数组有效存储内存
- 如何在cpp.中使用协议缓冲区存储大缓冲区/数组(char/int)
- 使用 pqxx 将 std::vector 存储在 postgresql 中,并从数据库中检索它
- 带结构的二维矢量:如何存储元素
- 添加存储在向量中的大整数的函数出现问题
- 从文件中读取多个字节,并将它们存储在C++中进行比较
- 在std::vector上存储带有模板的类实例
- 谷歌测试中的期望值存储在哪里
- 为什么C中的通用链表中存储的数据已损坏
- 在c++中获取两个大int,并将它们存储在数组中
- 在reactor中存储eventHandlers的最佳方式是什么