在指向具有生存期已结束的简单析构函数/标量类型的类类型的对象的指针上使用 delete-expression 的合法性



以下代码合法吗?

struct S
{};
int main()
{
S* p = new S;
p->~S();
delete p;
}

[basic.life#6] 中的标准规则:

在对象的生存期

开始之前,但在分配了对象将占用的存储之后24,或者在对象的生存期结束之后,在重新使用或释放对象占用的存储之前,可以使用表示对象将位于或曾经所在的存储位置地址的任何指针,但只能以有限的方式使用。有关正在构建或销毁的对象,请参见 [class.cdtor]。否则,此类指针引用已分配的存储([basic.stc.dynamic.allocation]),并且使用指针就像指针的类型为 void* 一样是明确定义的。允许通过此类指针进行间接寻址,但生成的左值只能以有限的方式使用,如下所述。在以下情况下,程序具有未定义的行为:

(6.1) — 对象将是或曾经是具有非平凡析构函数的类类型,指针用作删除表达式的操作数,

(6.2) — 指针用于访问对象的非静态数据成员或调用对象的非静态成员函数

[basic.life#6.1] 不适用,所以这段代码似乎是合法的,S因为它有一个微不足道的析构函数。

但是,根据 [basic.life#6.2],此代码是非法的,因为 delete-expression 调用S对象的析构函数,这是它的非静态成员函数。

编辑:根据[class.dtor#19],这也是非法的。

我应该听哪个?也许 [basic.life#6.2],因为 [basic.life#6.1] 没有规定对于具有平凡析构函数的类这样做必须是合法的。

结论是这是非法的。

但是,将S替换为标量类型后,情况似乎有所不同:

#include <memory>
int main()
{
char* p = new char;
std::destroy_at(p);
delete p;
}

这将对p指向的char对象进行伪析构函数调用,根据 [expr.call#5],这将结束其生存期:

如果后缀表达式

命名伪析构函数(在这种情况下,后缀表达式可能是用括号括起来的类成员访问),则函数调用将销毁由类成员访问的对象表达式表示的标量类型的对象([expr.ref],[basic.life])。

[basic.life#6.1] 仍然不适用。

但不同的是 [basic.life#6.2] 也不适用,因为没有为char对象调用非静态成员函数(char毕竟不是一个类)。

结论是它是合法的。

为什么?

似乎存在措辞缺陷。你的代码要么是合法的,要么不是。如果它打算合法,那么 [basic.life]/6.2 应该有一个例外,用于一个微不足道的析构函数。如果它不打算合法,那么"带有非平凡析构函数"的字样应该从 [basic.life]/6.1 中删除,因为即使析构函数是微不足道的,不可避免的析构函数调用也会违反 p6.2。

我怀疑这是合法的,因为在delete的情况下保留显式异常是没有意义的,如果不是为了防止破坏代码,实际上是在具有超出其生命周期的琐碎析构函数的对象上调用delete

N2762 删除了所有其他导致 UB 的操作的简单析构函数异常,这些操作指向其生存期之外的对象,但明显地将其保留用于删除表达式。这表明有必要在语言中保留此例外,以避免破坏代码。

最有可能的是,第二个项目符号没有更改为对微不足道的析构函数调用进行例外的原因是作者忘记了删除表达式确实调用了析构函数,即使它是微不足道的。他们可能认为delete被指定为跳过析构函数调用,如果它很简单。

(另请注意,在 C++98 到 C++17 中,S对象的生存期无论如何都不会由析构函数调用结束,因为 [basic.life]/1 指定析构函数调用仅在对象非平凡时才结束对象的生存期;这在 C++20 中由 CWG2256 更改。但是,由于 N2762,在 C++11 及更高版本中,如果对象的生存期尚未开始,因为它具有非平凡的初始化,我们可能会遇到类似于 OP 描述的问题。

我认为值得就此提交一份缺陷报告,尽管我不确定结果会是什么。自 N2762 以来已经过去了很长时间,我想委员会实际上会考虑此时是否应该消除琐碎析构函数的例外(修复 p6.1 而不是 p6.2);毕竟,CWG2256的分辨率也可能破坏了一些代码,但自从C++与 C 分道扬镳以来,这种代码可能变得越来越少见。

最新更新