现代 C++ 标准(C++20 或 C++23 都可以)的哪些部分规定不能使用放置 new 和显式析构函数调用来两次销毁对象?
alignas(T) std::byte storage[sizeof(T)];
T* const p = new (storage) T(...);
p->~T();
p->~T();
是否有任何条件允许这样做,例如如果
T
有一个简单的析构函数?
我发现这个之前的答案是一个半相关的问题,它给出了问题的琐碎析构函数部分的相对清晰的答案,但它使用C ++ 17,令我惊讶的是C ++ 20中的措辞似乎有更改为也禁止微不足道的析构函数这样做。对于琐碎的析构函数来说,这似乎应该没问题,所以我怀疑我错过了一些东西。
[basic.life]/6.2
...对象的生命周期结束后...程序具有未定义的行为,如果:
—指针用于...调用对象的非静态成员函数
[class.mem.special]/1
。
...或者,如果这是一个 伪析构函数调用 (即,如果
T
是非类),那么出于不同的原因它是 UB。 [expr.call]/4
表示它结束了对象的生命周期,如果没有对象(因为它的生命周期已经结束),则前提条件为 false,因此行为未定义。
是否有任何条件允许这样做,例如如果 T 有一个简单的析构函数?
好像不是这样的。
我认为这就是 C++20 的分解方式。
[basic.life]/1 表示对象的生命周期在其析构函数被调用时结束:
类型为
T
的对象o的生命周期结束于:
- 如果
是非类类型,则对象被销毁,或者T
- 如果
是类类型,则析构函数调用开始,或者T
- [...]
有两种情况:
对于非类
T
,我认为“对象被销毁”是在[expr.call]/5:中定义的
如果后缀表达式命名了伪析构函数(在这种情况下,后缀表达式可能是带括号的类成员访问),则函数调用将销毁由类成员访问的对象表达式表示的标量类型的对象。
对于类类型
T
,我们调用析构函数很简单。
[basic.life]/5 可能更清楚,
~T
在这两种情况下都结束了对象的生命周期:
程序可以通过显式调用对象的析构函数或伪析构函数来结束任何对象的生命周期。
[basic.life]/6 表示不能调用生命周期已结束的对象的非静态成员函数:
[A]在对象的生命周期结束后,在重用或释放该对象占用的存储空间之前,可以使用任何表示该对象将要或曾经位于的存储位置的地址的指针,但仅限于有限的情况方式。 [...] 允许通过此类指针进行间接寻址,但生成的左值只能以有限的方式使用,如下所述。如果出现以下情况,则程序具有未定义的行为:
- [...]
- 指针用于访问非静态数据成员或调用对象的非静态成员函数,或者
- [...]
因此,第二次调用析构函数是未定义的行为,因为它涉及调用对象的非静态成员函数(析构函数)。
…至少如果对象具有类类型。目前尚不清楚这是否准确地适用于非类对象,特别是考虑到上面提到的析构函数与伪析构函数的区别。
~int
真的是int
的成员函数吗?但是 HolyBlackCat 指出,也许这又被 [expr.call]/5 覆盖:它说伪析构函数调用结束了对象的生命周期,但前提条件是对象的生命周期已经结束真的见过吗?
在 C++17 中,[basic.life]/1 专门为第一点划出了一个例外,仅表示具有非平凡析构函数的类类型的生命周期在析构函数调用开始时结束。与 [basic.life]/5 类似。因此,在 C++17 中双重销毁普通可破坏类型是可以的。
这似乎是有意的更改:CWG 第 2256 期 表示,进行更改是为了使生命周期模型更加一致。它没有详细说明为什么这是可取的,而且我不确定我是否明白,因为据我所知,您仍然可以使用琐碎的类型做一些奇怪的终生事情,例如“重用它们的存储”而不破坏它们。