在对象显式销毁但内存未被释放之前调用成员函数是否合法?

24

我有这段代码:

struct data {
  void doNothing() {}
};

int main() {
    data* ptr = new data();
    ptr->~data();
    ptr->doNothing();
    ::operator delete(ptr);
}

请注意,doNothing()在对象被销毁后但其内存尚未被释放之前被调用。看起来“对象生命期”已经结束,但指针仍然指向正确分配的内存。该成员函数不访问任何成员变量。

在这种情况下,成员函数调用是否合法?


6
无论答案是什么,对于以后试图维护代码的任何人来说,这都会非常令人困惑。即使在某种情况下它能够合法(或者能够工作),也不要这样做。我认为这是不合法的,但自从我阅读过C++规范以来已经有一段时间了,所以可能是错误的。 - xxbbcc
4
如果你的数据结构真的没有销毁逻辑,为什么在调用成员函数之前要先调用析构函数?如果你期望析构函数会发生变化,那么你就不应该依赖于平凡行为。在更大型的应用程序中,数据和主函数中的代码可能位于不同的源文件中,一个开发人员更改其中一个文件时可能不知道另一个文件的内容。 - xxbbcc
@xxbbcc 假设这个调用来自于其他人编写的模板。 - sharptooth
2
非常有趣的问题;然而,我想知道 - 如果成员函数不访问任何成员,是否可以将其设置为static,使其与任何实例分离? - Codor
1
不,这是不合法的。官员已经被派遣了。 - Atsby
显示剩余2条评论
3个回答

31

是的,在OP中的代码情况下。因为析构函数是平凡的,调用它并不会结束对象的生命周期。[basic.life]/p1:

当类型为 T 的对象的生命期结束时:

  • 如果 T 是一个带有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或者
  • 对象所占用的存储被重用或释放。

[class.dtor]/p5:

如果析构函数不是用户提供的,并且:

  • 析构函数不是虚拟的,
  • 其类的所有直接基类都具有平凡析构函数,并且
  • 对于其类的所有非静态数据成员,如果它们是类类型(或数组),则每个这样的类都有平凡的析构函数。

不,一般情况下是不可以这样做的。在对象的生命周期结束后调用非静态成员函数是未定义行为。[basic.life]/p5:

在对象的生命周期结束之后,在重新使用或释放该对象所占用的存储之前,任何指向该对象所在存储位置的指针都可以以有限的方式使用。对于正在构造或析构的对象,请参见 12.7。否则,这样的指针引用分配的存储(3.7.4.2),并且将指针用作类型为 void* 的指针是定义良好的。虽然可以通过此类指针进行间接寻址,但由此产生的 lvalue 只能以有限的方式使用,如下所述。如果程序是以下情况之一,则其行为未定义:

  • [...]
  • 指针用于访问对象的非静态数据成员或调用非静态成员函数,或
  • [...]

1
由于这个微不足道的对象的生命周期不会随着析构函数的调用而结束,那么我可以多次显式地销毁它吗? - Baum mit Augen
根据您最后引用的内容,OP调用了“对象的非静态成员函数”,因此OP代码确实具有未定义的行为! - cmaster - reinstate monica
@BaummitAugen 这很有趣。在[class.dtor]中,紧接着我在答案中引用的部分是“如果为已经结束生命周期的对象调用了析构函数,则其行为未定义(3.8)。[例如:如果显式调用自动对象的析构函数,并且随后以通常会调用对象的隐式销毁方式离开块,则其行为未定义。 ——结束示例]”该示例表明,“不再存在”与“生命周期已结束”是同义词。不是吗? - Barry
1
只有当对象的生命周期结束时,@cmaster 才会起作用。 - T.C.
用户提供的是什么意思? - Random832
显示剩余3条评论

5
给定 [class.dtor]:
一旦为对象调用析构函数,对象就“不存在”了。
这段引自 [basic.life]:
或者,在对象的生命周期结束之后,并在重新使用或释放对象所占用的存储空间之前,任何指向对象将被或已经被定位的存储位置的指针都可以使用,但只能以有限的方式…如果程序符合以下条件,则具有未定义的行为:
- …
- 指针用于访问对象的非静态数据成员或调用对象的非静态成员函数
规定你所拥有的是未定义的行为。但是,这里有不同的语言 - “对象已经不存在”与“对象已经结束”,在[basic.life]中的早期,它指出:
它的初始化完成。 当类型为 T 的对象的生命周期结束时: - 如果 T 是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或 - 对象占用的存储空间被重用或释放。
一方面,您没有非平凡的析构函数,因此[basic.life]表明对象的生命周期尚未结束 - 存储空间尚未被重用或释放。另一方面,[class.dtor]表明对象“不存在”,这肯定听起来应该是“结束”的同义词,但实际上并不是。
我想“法律语言家”的答案是:从技术上讲,它不是未定义的行为,并且似乎非常合法。而“代码质量”的答案则是:不要这样做,最好不要令人困惑。

1
有趣的是,据我所知,[class.dtor] 这句话是标准中唯一将对象称为“存在”的部分。 - Barry

4
其他答案是正确的,但遗漏了一个细节:
如果析构函数或构造函数中有一个是平凡的,则允许使用。其他答案已经清楚地解释了如果析构函数是平凡的,则原始对象的生命周期尚未结束。
但是,如果构造函数是平凡的,则只要存在适当大小和对齐方式的内存位置,就存在一个对象。因此,即使具有非平凡析构函数和平凡构造函数,也存在全新的对象,您可以在其上调用成员。
其他答案遗漏的措辞,紧接着他们引用的生命周期规则之前,说:
对象的生命周期是对象的运行时属性。如果对象是类或聚合类型,并且它或其中一个成员通过除平凡默认构造函数以外的构造函数进行初始化,则称该对象具有非空初始化。[注意:通过平凡的复制/移动构造函数进行初始化是非空初始化。-注]类型T的对象的生命周期从以下时刻开始:
存储具有适当对齐和大小以容纳类型T的存储器,并且
如果对象具有非空初始化,则其初始化完成。
关于在旧销毁的存储中平凡创建的新对象的使用的重要说明:由于平凡的构造,数据成员上没有执行任何初始化,因此它们现在都具有不确定的值,因此您必须在读取任何值之前设置它们的值(通过初始化或调用不使用先前值的赋值运算符)。在OP的情况下,原始对象仍然存在,因此不适用此警告。

正要指出构造函数的同样问题。+1 - user541686

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接