【问题标题】:What is the relationship between the end of object's lifetime and when it ceases to exist?对象生命周期的结束与它何时停止存在之间的关系是什么?
【发布时间】:2020-12-25 10:52:20
【问题描述】:

在下面的简短示例中,关于指针f 指向或用于在从main 返回之前指向的对象可以说什么?

#include <vector>

struct foo {
    std::vector<int> m;
};

int main()
{
    auto f = new foo;
    f->~foo();
}

我相信 foo 不再是 f 曾经指向的对象。我收到很多 cmets 说这可能不正确,而是可能有一个对象 foo 处于已破坏、死亡或其他无效状态。

对于显式销毁但其存储仍然有效的对象的存在,语言标准有什么说法?

换句话说,是否可以合理地说f 处仍有一个对象超出其生命周期?有没有对象不在其生命周期内,没有开始构造,也没有被销毁?


编辑:

很明显,当对象不在其生命周期内时,它也可以存在。在构造和销毁期间,有一个对象,它的生命周期尚未开始或已经结束。来自https://timsong-cpp.github.io/cppwp/intro.object#1

[...] 一个对象在其构建期间([class.cdtor])、在其整个生命周期以及在其销毁期间([class.cdtor])占用一个存储区域。 [...]

但是在f-&gt;~foo(); 之后,f 指向的对象(我们称之为o)没有被构造,它不在其生命周期内,也没有被破坏。我对本节的阅读是 o 不能再占用存储空间,因为它不在任何枚举的情况下。似乎这意味着不再有 o 并且不再有指向 o 的指针。相反,如果您有一个指向 o 的指针,那么该指针将指向 o 无法占用的存储空间。


编辑 2:

如果不再有对象,那么foo 有什么样的价值?似乎它可以拥有的唯一合理的可能值是指向对象的指针,这与陈述相矛盾。见this question

【问题讨论】:

  • 来自 C++20 eel.is/c++draft/basic.life#5 我相信适用,但我认为在那之前情况有所不同——不确定。
  • @N.Shead 有趣的是,如果您依赖副作用,则只有 UB 不调用析构函数。虽然它与问题无关,但似乎如果我在f 指向的对象上执行放置new,这将是明确定义的行为,但也会泄漏f-&gt;m 可能持有的任何内存?或者我会被认为是依赖它会被释放的副作用?
  • 我想这取决于您是否认为释放内存是一种副作用。据我所知,除了“写入文件”之外,该标准对于副作用的确切含义非常宽松。我怀疑这通常是 UB,虽然我不能肯定地说。
  • @FrançoisAndrieux f-&gt;m 使用的任何内存都会在 ~foo() 析构函数中释放,无论是手动调用还是自动调用。在发布的代码中泄漏的是new 分配的对象内存。为了清理它,您需要在 f 上放置 new,然后在 delete f; 上放置。
  • @dxiv 我之前的评论是假设如果f-&gt;~foo();new (f) foo; 替换并在之后尽最大努力清理并且f-&gt;m 有容量。看到我以 “虽然它与问题无关” 开头那部分。在那种假设的情况下,我不清楚是 UB 还是定义明确但第一个 foo 泄露了它的成员的内部资源。

标签: c++ language-lawyer lifetime object-lifetime


【解决方案1】:

在 C++ 中,对象本质上是永恒的。语言中没有任何东西可以使对象消失。超出其生命周期的对象仍然是一个对象,它仍然占用存储空间,并且标准有 specific things,您可以使用指向超出其生命周期的对象的指针/引用来执行此操作。

一个对象只有在不可能有​​一个有效的指针/引用时才会真正消失。当该对象占用的存储结束其存储持续时间时,就会发生这种情况。超过其持续时间的指向存储的指针是无效指针,即使地址本身稍后再次变为有效。

因此,通过调用析构函数而不是使用delete f(这也会释放存储空间),f 仍然指向foo 类型的对象,但该对象超出了它的生命周期。


我上述陈述的理由基本上归结为标准没有没有条规定,以支持未创建对象的概念。

对象取消在哪里?

该标准提供了关于对象何时存在于一块存储中的清晰、明确的陈述。 [intro.object]/1 概述了引发对象创建的确切机制。

该标准提供了关于对象生命周期何时开始和结束的清晰、明确的陈述。 [basic.life] 完全概述了这些事情,但 [basic.life]/1 特别说明了对象的生命周期何时开始和结束。

标准提供关于对象何时不再存在的任何声明(明确或其他)存在。该标准说明了对象的创建时间、生命周期的开始时间和结束时间。但是从来没有说它们何时停止存在于一块存储中。

也有人讨论过形式的陈述:

任何表示对象将或所在的存储位置地址的指针都可以使用,但仅限于有限的方式。

添加了重点。

过去时的使用表明该对象不再位于该存储中。但是该物体何时停止位于那里?关于究竟是什么导致了这种情况的发生,没有明确声明。没有它,这里使用过去时就没有关系了。

如果您不能指出它何时停止存在的声明,那么您最多只能说标准中有几个地方的措辞可以清理。这并没有消除标准没有说明对象何时停止存在这一明确事实。

指针有效性

但它确实说明了何时不再可访问对象。

为了使对象不再存在,标准必须考虑指向那些不再存在的对象的指针。毕竟,如果一个指针指向一个对象,那么那个对象一定还存在,对吧?

[basic.compound]/3 概述了指针可以具有的状态。指针可以处于以下四种状态之一:

  • 指向对象或函数的指针(该指针被称为指向对象或函数),或
  • 超过对象末尾的指针 ([expr.add]),或
  • 该类型的空指针值 ([conv.ptr]),或
  • 一个无效的指针值。

没有允许指向不指向对象的指针。允许“无效指针值”,但指针仅在 the storage duration for the storage they point into ends 时变为无效:

当一个存储区域的持续时间结束时,表示该存储区域任何部分地址的所有指针的值都变为无效指针值。

请注意,此语句意味着所有指向此类对象的指针都不再处于“指向对象的指针”状态并进入“无效指针”状态。因此,此类存储中的对象(包括其生命周期之内和生命周期之外)将不再可访问。

这正是标准需要存在的那种声明,以支持不再存在的对象的概念。

但不存在这样的说法。

[basic.life] 确实有几个语句解决了可以使用指向其生命周期之外的对象的指针的有限方式。但请注意它使用的具体措辞:

对于正在构造或销毁的对象,请参阅 [class.cdtor]。否则,这样的指针指向已分配的存储空间 ([basic.stc.dynamic.deallocation]),并且像使用 void* 类型的指针一样使用指针是明确定义的。

从不说指针“指向”分配的存储。它永远不会撤销 [basic.compound]/3 关于指针种类的声明。指针仍然是指向对象的指针;只是指针“指的是分配的存储空间”。并且该指针可以用作void*

也就是说,不存在“指向已分配存储空间的指针”之类的东西。有一个“指向超出其生命周期的对象的指针,其指针值可用于引用分配的存储空间”。但仍然是“指向对象的指针”。

生命不存在

对象必须存在才能拥有生命周期。该标准清楚地表明了这一点。但是,该标准在任何时候都没有将对象的存在与其生命周期联系起来。

确实,如果结束一个对象的生命周期意味着该对象不存在,那么对象模型会简单得多。 [basic.life] 的大部分内容都是关于制定特定方式,您可以在该对象的生命周期之外使用对象的名称或指向它的指针/引用。如果对象本身不存在,我们就不需要那种东西。

关于此事的讨论中陈述如下:

我相信提到过期对象是为了解释正在构造的对象和正在销毁的对象。

如果是这样,[basic.life]/8 talking about with this statement 是什么:

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用,或原始对象的名称

如果指向原始对象的指针在对象的生命周期结束时变成指向已分配内存的指针,为什么这句话要谈论指向原始对象的指针?指针不能指向不存在的对象,因为它们不存在

只有当这些对象在其生命周期之外继续存在时,这段话才有意义。不,这不仅仅是在构造函数/析构函数中;本节中的示例非常清楚地说明了这一点:

struct C {
  int i;
  void f();
  const C& operator=( const C& );
};

const C& C::operator=( const C& other) {
  if ( this != &other ) {
    this->~C();                 // lifetime of *this ends
    new (this) C(other);        // new object of type C created
    f();                        // well-defined
  }
  return *this;
}

C c1;
C c2;
c1 = c2;                        // well-defined
c1.f();                         // well-defined; c1 refers to a new object of type C

虽然operator= 确实调用了析构函数,但析构函数在使用this 指针之前完成。因此,[class.cdtor] 的特殊规定在新对象创建时不适用于this。因此,新对象是在对旧对象的析构函数调用之外创建的。

所以很明显,对象的“超出其生命周期”规则意味着总是有效。这不仅仅是对构造函数/析构函数的规定(如果是的话,它会明确地指出这一点)。这意味着在创建新对象之前,名称/指针/引用仍然必须在其生命周期之外命名/指向/引用对象。

为此,他们命名/指向/引用的对象必须仍然存在

【讨论】:

  • 一个超出其生命周期的对象仍然是一个对象,它仍然占用存储空间 This不同意
  • 澄清它不同意的地方:对象仍然占用存储空间,而不是它仍然存在。
  • 感谢您的回答。这对我来说都是有意义的,除了根据this“一个对象在其构建期间([class.cdtor])、整个生命周期和销毁期间([ class.cdtor])。”.因此,在~foo() 之后,该对象既没有被构造,也没有在其生命周期内被破坏,因此它不能占用存储空间。如果你能把它和答案调和起来,它肯定会回答这个问题。
  • @Eljay 这个问题有language-lawyer 标签,所以它应该只与C++ 虚拟机有关。
  • 我相信您引用的标准与您的结论不一致。 “[...] 对象将位于或曾经位于的存储位置的地址 [...]”。这对我来说意味着一个类指针已经变成了一个指向存储的指针,而不是一个指向对象的指针,因为对象不再存在。
猜你喜欢
  • 1970-01-01
  • 2012-04-25
  • 1970-01-01
  • 2013-06-11
  • 2014-11-20
  • 2013-08-22
  • 2012-03-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多