为什么销毁被 placement new 覆盖的对象不是未定义的行为?

Why isn't it undefined behaviour to destroy an object that was overwritten by placement new?

我想弄清楚以下是否是未定义的行为。我感觉它不是 UB,但我对标准的阅读使它看起来像是 UB:

#include <iostream>

struct A {
    A() { std::cout << "1"; }
    ~A() { std::cout << "2"; }
};

int main() {
    A a;
    new (&a) A;
}

引用C++11标准:

basic.life¶4 说 "A program may end the lifetime of any object by reusing the storage which the object occupies"

所以在 new (&a) A 之后,原来的 A 对象已经结束了它的生命周期。

class.dtor¶11.3 表示 "Destructors are invoked implicitly for constructed objects with automatic storage duration ([basic.stc.auto]) when the block in which an object is created exits ([stmt.dcl])"

因此当 main 退出时隐式调用原始 A 对象的析构函数。

class.dtor¶15 说 "the behavior is undefined if the destructor is invoked for an object whose lifetime has ended ([basic.life])."

所以这是未定义的行为,因为原来的 A 不再存在(即使新的 a 现在存在于同一存储中)。

问题是是否调用了原始 A 的析构函数,或者是否调用了当前名为 a 的对象 的析构函数。

我知道 basic.life¶7,它说名称 a 指的是放置 new 之后的新对象。但是 class.dtor¶11.3 明确表示它是退出作用域 对象的析构函数,而不是名称引用的 对象的析构函数退出范围.

我是不是误读了标准,或者这实际上是未定义的行为?

编辑:几个人告诉我不要这样做。澄清一下,我绝对不打算在生产代码中这样做!这是一个 CppQuiz 问题,它是关于极端情况而不是最佳实践。

你误会了。

"Destructors are invoked implicitly for constructed objects" …意思是那些存在并且它们的存在已经完成了建造。虽然可以说没有完全拼出来,但原来的 A 不符合这个标准,因为它不再是 "constructed":它根本不存在!只有 new/replacement 对象会自动销毁,然后在 main 结束时如您所料。

否则,这种放置 new 的形式将非常危险,并且在语言中具有值得商榷的价值。然而,值得指出的是,以这种方式重新使用实际的 A 有点奇怪和不寻常,如果没有其他原因,只是它会导致这种问题。通常你会将 placement-new 放入一些平淡的缓冲区(比如 char[N] 或一些对齐的存储),然后你自己也调用析构函数。

实际上可能会在 basic.life¶8 找到与您的示例类似的内容 — 它是 UB,但这只是因为有人在 B 之上构建了 T;措辞非常清楚地表明这是代码的唯一问题。

但关键是:

The properties ascribed to objects throughout this International Standard apply for a given object only during its lifetime. [..] [basic.life¶3]

评论太长了。

Lightness 的回答是正确的,他的 link 是正确的参考。

但让我们更准确地检查术语。有

  • "Storage duration",关于内存。
  • "Lifetime",关于对象。
  • "Scope",关于名字。

对于自动变量三者重合,这就是为什么我们经常分不清的原因:A"variable goes out of scope"。即:名称超出范围;如果它是一个自动存储持续时间的对象,则调用析构函数,结束命名对象的生命周期;最后内存被释放。

在您的示例中,只有 name scopestorage duration 重合——在其存在期间的任何时候,名称 a 指代到有效内存 — ,而 对象生命周期 在同一内存位置并具有相同名称 a.

的两个不同对象之间拆分

不,我认为您无法将 11.3 中的 "constructed" 理解为 "fully constructed and not destroyed",因为如果对象的生命周期,dtor 将被再次 (错误地)调用被前面的显式析构函数调用提前结束。 事实上,这是内存重用概念的一个问题:如果新对象的构造因异常而失败,则范围将被保留,并且将尝试对不完整的对象或旧对象调用析构函数已经删除了。

我想您可以想象自动分配的、类型化的内存,标记有标签 "to be destroyed",在展开堆栈时对其进行评估。除了这个简单的概念之外,C++ 运行时并没有真正跟踪单个对象或它们的状态。由于变量名基本上是常量地址,因此很容易想到 "the name going out of scope" 触发对假设类型的命名对象的析构函数调用,该对象假定存在于该位置。如果这些假设之一是错误的,则所有投注均无效。

Am I misreading the standard, or is this actually undefined behaviour?

None 个。该标准并不明确,但可以更明确。其意图是调用新对象的析构函数,如 [basic.life]p9.

中所暗示的那样

[class.dtor]p12 不是很准确。我问了 Core,Mike Miller (a very senior member) said:

I wouldn't say that it's a contradiction [[class.dtor]p12 vs [basic.life]p9], but clarification is certainly needed. The destructor description was written slightly naively, without taking into consideration that the original object occupying a bit of automatic storage might have been replaced by a different object occupying that same bit of automatic storage, but the intent was that if a constructor was invoked on that bit of automatic storage to create an object therein - i.e., if control flowed through that declaration - then the destructor will be invoked for the object presumed to occupy that bit of automatic storage when the block is exited - even it it's not the "same" object that was created by the constructor invocation.

我会在 CWG 问题发布后立即更新此答案。所以,你的代码没有UB。

想象一下,使用 placement new 为 A a 对象所在的存储空间创建一个 struct B。在范围的末尾,将调用 struct A 的析构函数(因为 A 类型的变量 a 超出范围),即使类型 B 的对象在现在住在那里的房地产。

如前所述:

"If a program ends the lifetime of an object of type T with static ([basic.stc.static]), thread ([basic.stc.thread]), or automatic ([basic.stc.auto]) storage duration and if T has a non-trivial destructor,39 the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place;"

所以将B放入a存储后,需要销毁B并重新放一个A,才不会违反上面的规则。这在某种程度上不适用于此处,因为您将 A 放入 A,但它显示了行为。它表明,这种想法是错误的:

So the destructor for the original A object is invoked implicitly when main exits.

不再有 "original" 对象。 a 的存储中只有一个对象当前处于活动状态。就是这样。而在当前位于a的数据上,调用了一个函数,即A的析构函数。这就是程序编译的结果。如果它能神奇地跟踪所有 "original" 对象,您将以某种方式拥有动态运行时行为。

此外:

A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression ([expr.delete]) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.

由于 A 的析构函数不是微不足道的并且有副作用,(我认为)它的未定义行为。对于内置类型,这不适用(因此您可以使用 char 缓冲区作为对象缓冲区,而无需在使用后将 chars 重建回缓冲区),因为它们有一个简单的(无操作)析构函数。