为什么销毁被 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 scope 和 storage 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 重建回缓冲区),因为它们有一个简单的(无操作)析构函数。
我想弄清楚以下是否是未定义的行为。我感觉它不是 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 scope 和 storage 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 重建回缓冲区),因为它们有一个简单的(无操作)析构函数。