内存重用和UB

Memory reusing and UB

本文关键字:UB 内存      更新时间:2023-10-16

以下代码是否生成UB?

#include <iostream>
#include <climits>
struct A
{
    ~A(){ std::cout << "Non-trivial" << std::endl; }
};
int main()
{
    A a;
    new (&a) A; 
}   //UB?

演示

标准N3797::3.8/8 [basic.life]

如果程序以static结束T类型对象的生存期(3.7.1)、线程(3.7.2)或自动(3.7.3)存储持续时间,如果T有一个非平凡的析构函数,程序必须确保当发生隐式析构函数调用;否则程序未定义。

我认为在main函数的左边没有UB,因为&a指向与最初分配的类型相同的类型。

[3.8/4]程序可以通过重用对象占用的存储,或者通过显式调用具有非平凡析构函数的类类型对象的析构函数,来结束任何对象的生命周期。对于具有非平凡析构函数的类类型的对象,在对象所占用的存储被重用或释放之前,程序不需要显式调用析构函数;但是,如果没有对析构函数的显式调用,或者如果删除表达式(5.3.5)没有用于释放存储,则不应隐式调用析构函数,并且任何依赖于析构函数产生的副作用的程序都具有未定义的行为。

你的程序"依赖"析构函数产生的副作用吗?清楚:

  • 可观察程序的输出在变化的意义上取决于析构函数是否运行

  • 如果析构函数不运行,则cout缓冲区的状态、位置等将不同,并且

  • 在程序终止时留下来执行的刷新动作可能不同。

可以合理地说,后面的程序行为/流取决于析构函数是否运行,这意味着技术上未定义的行为。

尽管如此,我还是敢说,标准的目的是警告析构函数操作,如不释放锁或减少引用计数器,可能会产生令人不快的后果,如以后挂起或泄漏,并且在实践中,在所有实际的编译器上,您发布的程序将如您所期望的那样运行。

同样值得注意的是,通常情况下,如果构造函数抛出并且范围在该内存位置没有有效对象的情况下展开,那么这种做法是危险的。

这是未定义的行为,但不是因为您引用的原因:

[基本寿命]/4

[…]如果没有显式调用析构函数或如果删除表达式(5.3.5)未用于释放存储,则析构函数和任何依赖于在析构函数产生的副作用上有未定义的行为。

由于析构函数有副作用,在重用内存之前,必须显式调用析构函数

A a;
a.~A();
new (&a) A;