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

时间:2015-05-18 18:21:17

标签: c++ destructor language-lawyer object-lifetime explicit-destructor-call

我有这段代码:

struct data {
  void doNothing() {}
};

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

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

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

3 个答案:

答案 0 :(得分:31)

是的,就OP中的代码而言。因为析构函数是微不足道的,所以调用它并不会终止对象的生命周期。 [basic.life] / P1:

  

类型T的对象的生命周期在以下时间结束:

     
      
  • 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用将启动,或者
  •   
  • 对象占用的存储空间被重用或释放。
  •   

[class.dtor] / P5:

  

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

,则析构函数很简单      
      
  • 析构函数不是virtual
  •   
  • 其类的所有直接基类都有简单的析构函数,
  •   
  • 对于类类的所有非静态数据成员(或其数组),每个类都有一个小问题   析构函数。
  •   

不,不是一般情况。在对象的生命周期结束后调用非静态成员函数是UB。 [basic.life] / P5:

  

[A]对象的生命周期已经结束并且在存储之前   被占用的对象被重用或释放,任何引用的指针   对象将位于或位于的存储位置可能是   使用但仅限于有限的方式。对于正在建造或销毁的物体,见12.7。否则,这样的指针指的是已分配   存储(3.7.4.2),并使用指针,就像指针一样   类型void*,定义明确。通过这样的指针间接是   允许但最终的左值只能以有限的方式使用,   如下所述。该程序尚未定义   行为如果:

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

答案 1 :(得分:5)

鉴于[class.dtor]:

  

为对象调用析构函数后,对象不再存在

来自[basic.life]的片段:

  

...或者,在对象的生命周期结束后并且在对象占用的存储空间之前   重用或释放任何指向对象所在或位于的存储位置的指针   可能会被使用,但只能以有限的方式使用......如果出现以下情况,该程    - ......
   - 指针用于访问非静态数据成员或调用非静态成员函数   对象

规定你所拥有的是未定义的行为。然而,这里有不同的语言 - “对象不再存在”与“对象已经结束”,而在[basic.life]中更早,它表示:

  

初始化完成。   在以下情况下,T 类型的对象的生命周期结束:    - 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用将启动,或者    - 对象占用的存储空间被重用或释放。

一方面,你没有一个非平凡的析构函数,所以[basic.life]表明对象的生命周期还没有结束 - 存储没有被重用或释放。另一方面,[class.dtor]建议对象“不再存在”,这听起来应该是“已结束”的同义词,但事实并非如此。

我认为“语言 - 律师”的答案是:它在技术上并非未定义的行为,看起来完全合法。 “代码质量”的答案是:不要这样做,它充其量是令人困惑的。

答案 2 :(得分:4)

其他答案是正确的,但遗漏了一个细节:

如果析构函数或构造函数是微不足道的,则允许。其他答案清楚地解释了如果析构函数是微不足道的,那么原始对象的生命周期还没有结束。

但是如果构造函数是微不足道的,那么只要存在适当大小和对齐的内存位置,就会存在一个对象。因此,即使使用非平凡的析构函数和琐碎的构造函数,也可以使用一个全新的对象来调用成员。

其他答案遗漏的措辞,即紧接在他们引用的寿命终止规则之前,

  

对象的生命周期是对象的运行时属性。如果一个对象属于类或聚合类型,并且它或其成员之一由除了普通默认构造函数之外的构造函数初始化,则称该对象具有非空初始化。 [注意:通过简单的复制/移动构造函数进行初始化是非空的初始化。 - 结束注释]类型为T的对象的生命周期始于:

     
      
  • 获得具有T类型的正确对齐和大小的存储,并且
  •   
  • 如果对象具有非空的初始化,则其初始化完成。
  •   

关于在旧的被破坏的存储中存储的新对象的使用的重要说明:由于简单的构造,没有对数据成员执行初始化,并且它们现在都具有不确定的值,因此您必须设置在读取任何值之前,它们的值(通过初始化或不使用前一个值的赋值运算符的调用)。

在OP的情况下,原始对象仍然存在,因此这个警告不适用。