24

我有这个代码:

struct data {
  void doNothing() {}
};

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

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

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

4

3 回答 3

31

是的,对于 OP 中的代码。因为析构函数是微不足道的,调用它并不会结束对象的生命周期。[基本生活]/p1:

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

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

[class.dtor]/p5:

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

  • 析构函数不是virtual
  • 其类的所有直接基类都有微不足道的析构函数,并且
  • 对于其类的所有属于类类型(或其数组)的非静态数据成员,每个这样的类都有一个微不足道的析构函数。

不,不是在一般情况下。在对象的生命周期结束后调用非静态成员函数是 UB。[基本生活]/p5:

[A] 在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,任何指向对象将要或曾经位于的存储位置的指针都可以使用,但只能以有限的方式使用。对于正在建造或破坏的物体,见 12.7。否则,这样的指针指的是分配的存储(3.7.4.2),并且使用指针就像指针是 type 一样void*,是明确定义的。允许通过这种指针进行间接访问,但生成的左值只能以有限的方式使用,如下所述。如果出现以下情况,该程序具有未定义的行为:

  • [...]
  • 指针用于访问非静态数据成员或调用对象的非静态成员函数,或
  • [...]
于 2015-05-18T18:29:10.740 回答
5

给定 [class.dtor]:

一旦为对象调用析构函数,该对象就不再存在

这个片段来自 [basic.life]:

...或者,在对象的生命周期结束之后,并且在重用或释放对象占用的存储空间之前,可以使用任何指向该对象将要或曾经位于的存储位置的指针,但只能以有限的方式使用... 程序在以下情况下具有未定义的行为:
— ...
— 指针用于访问非静态数据成员或调用对象的非静态成员函数

规定你所拥有的是未定义的行为。但是,这里有不同的语言 - “对象不再存在”与“对象已结束”,在 [basic.life] 的早期,它指出:

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

一方面,您没有重要的析构函数,因此 [basic.life] 表明对象的生命周期尚未结束——存储尚未被重用或释放。另一方面,[class.dtor] 表明对象“不再存在”,这听起来应该是“结束”的同义词,但事实并非如此。

我想“语言律师”的答案是:从技术上讲,这不是未定义的行为,而且似乎完全合法。“代码质量”的答案是:不要这样做,它充其量是令人困惑的。

于 2015-05-18T18:34:05.977 回答
4

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

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

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

其他答案遗漏的措辞,紧接在他们引用的生命周期结束规则之前,说

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

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

关于使用在旧销毁对象的存储中平凡创建的新对象的重要注意事项:由于平凡的构造,尚未对数据成员执行初始化,并且它们现在都具有不确定的值,因此您必须设置它们的值(在读取任何内容之前,通过初始化或调用不使用先前值的赋值运算符)。

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

于 2015-05-19T00:54:25.423 回答