【问题标题】:Is it legal to call member functions after an object has been explicitly destroyed but before its memory was deallocated?在对象被显式销毁但在其内存被释放之前调用成员函数是否合法?
【发布时间】:2015-07-30 09:02:13
【问题描述】:

我有这个代码:

struct data {
  void doNothing() {}
};

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

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

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

【问题讨论】:

  • 不管答案如何,这对于以后尝试维护代码的任何人来说都是非常混乱的。不要这样做,即使它以某种方式证明是合法的(或在特定情况下工作)。我想说这是不合法的,但我已经有一段时间没有阅读 C++ 规范了,我可能错了。
  • 如果你的数据结构真的没有析构逻辑,为什么要先调用析构函数再调用成员函数。如果您期望析构函数发生变化,则不应依赖于琐碎的行为。在较大的应用程序中,主文件中的数据和代码可能位于不同的源文件中,而更改其中一个的开发人员可能不知道另一个的内容。
  • @xxbbcc 假设调用来自其他人编写的模板。
  • 非常有趣的问题;但是,我想知道 - 如果成员函数不访问任何成员,是否可以将其设为 static 使其与任何实例断开连接?
  • 不,这是不合法的。已派出警官。

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


【解决方案1】:

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

T 类型对象的生命周期结束于:

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

[class.dtor]/p5:

如果析构函数不是用户提供的并且满足以下条件,则它是微不足道的:

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

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

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

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

【讨论】:

  • 那么既然平凡对象的生命周期不会随着析构函数的调用而结束,我是否可以多次显式销毁它?
  • OP 调用“对象的非静态成员函数”,因此根据您最后的引用,OP 代码确实具有未定义的行为!
  • @BaummitAugen 这很有趣。在 [class.dtor] 中,紧接在我的回答中引用的部分是“如果为生命周期已结束的对象调用析构函数(3.8),则行为未定义。[示例:如果自动对象的析构函数是显式调用,并且随后以通常会调用对象的隐式破坏的方式留下块,行为未定义。-结束示例]” 该示例表明“不再存在”同义词与“生命已结束”。没有?
  • @cmaster 仅当对象的生命周期结束时。
  • @Barry 我猜不是。此答案中的第一句话证明了生命周期没有结束。连同您的报价,这意味着您实际上可以多次破坏这样的对象。我觉得这个例子在这里不适用。确实很有趣。
【解决方案2】:

给定 [class.dtor]:

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

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

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

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

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

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

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

【讨论】:

  • 有趣的是,来自 [class.dtor] 的短语是标准中唯一将对象称为“现有”的部分(据我所知)。
【解决方案3】:

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

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

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

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

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

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

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

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

【讨论】:

  • 正要指出关于构造函数的同样事情。 +1
猜你喜欢
  • 2015-02-03
  • 1970-01-01
  • 2011-12-07
  • 1970-01-01
  • 2018-05-02
  • 2021-04-19
  • 2013-09-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多