【问题标题】:Difference in the end of lifetime rules?生命终结规则的差异?
【发布时间】:2019-05-16 01:59:02
【问题描述】:

https://en.cppreference.com/w/cpp/language/lifetimeNotes 部分有这段代码,在此转载:

struct A {
  int* p;
  ~A() { std::cout << *p; } // if n outlives a, prints 123
};
void f() {
  A a;
  int n = 123; // if n does not outlive a, this is optimized out (dead store)
  a.p = &n;
}

Notes 部分想要表达什么?

据我了解,代码是 UB(或者是),因为很明显 n 不会比 a 寿命长。

这是什么意思:

非类对象之间的生命周期结束规则的差异(结束 存储期限)和类对象(构造的相反顺序) 很重要

但它并没有说重要如何

我对这整个部分感到非常困惑。

【问题讨论】:

  • 我不确定他们在注释部分想说什么。 AFAIK 这是符合标准的 UB。
  • @NathanOliver 我认为这可能暗示n 确实比a 活得更久,因为a 在右大括号之前被破坏,而n 在右大括号处被破坏。不过,我不相信这是真的。
  • 我认为逆序销毁意味着na之前被销毁,导致UB
  • 这是core issue 2256。我们从有关该主题的核心反射器帖子之一中获取了这个示例(Clang 根据当前规则“错误编译”)。

标签: c++ language-lawyer undefined-behavior


【解决方案1】:

这是 C++ 生命周期规则的一个奇怪方面。 [basic.life]/1 tells us 对象的生命周期结束:

  • 如果T 是具有非平凡析构函数的类类型 ([class.dtor]),则析构函数调用开始,或者
  • 对象占用的存储空间被释放,或被未嵌套在 o ([intro.object]) 中的对象重用。

添加了重点。 int 不是“具有非平凡析构函数的类类型”,因此它的生命周期仅在其占用的存储被释放时结束。相比之下,A 是一个具有非平凡析构函数的类类型,因此它的生命周期在析构函数被调用时结束。

作用域退出时释放作用域的存储,依据[basic.stc.auto]/1

[具有自动存储持续时间的变量]的存储将持续到创建它们的块退出。

但是自动变量按照[stmt.jump]/2销毁:

从范围退出时(无论如何完成),已在该范围内构建的具有自动存储持续时间的对象将按照其构建的相反顺序销毁。

注意指定了销毁顺序,但没有指定自动存储释放的顺序。这意味着实现可以在每个变量被销毁后立即释放存储空间,或者稍后一次性释放,或者以其他任意顺序释放。

现在,它使用单数存储(“存储用于...持续”)而不是单独讨论每个变量的事实可能表明其意图是一次性释放整个存储那个范围。但标准中没有明确说明这一点。因此,只要变量在其存储被释放之前被销毁,任何销毁与释放的顺序似乎都是合法的。

这意味着代码完全有可能工作,na 寿命长。但不确定它是否确实有效。

【讨论】:

  • @SergeyA:经过进一步调查,事情并没有那么简单。我的答案已相应修改。
  • @Galik:“我仍然(从那个措辞)不明白为什么每个对象不会被破坏然后依次释放。”我不是说它不能;我是说它不是必需的。这就是为什么 cppreference 页面以假设的方式谈论它是否有效。因为它是未指定的。
  • @Brian:“我们”是谁?委员会可以吗?当然;对此进行了一些讨论。 SO上的随机用户可以吗?没有。
  • 我所说的“我们”是指 C++ 社区,向委员会提议做出这样的改变。我所说的“可以”是指这是否有很大的机会被接受。
  • @SornelHaetir 这被 as-if 覆盖了。
【解决方案2】:

这个例子是从Core Language Issue 2256借来的:

章节: 6.8 [basic.life] 状态: 起草提交者: Richard Smith 日期: 2016- 03-30

根据 6.4 [basic.lookup] 项目符号 1.4,以下示例已定义行为,因为 n 的生命周期会延长到其存储空间被释放,即在 a 的析构函数运行之后:

  void f() { 
    struct A { int *p; ~A() { *p = 0; } } a; 
    int n; 
    a.p = &n; 
  } 

如果所有对象的生命周期结束时,无论它们是否具有非平凡的析构函数,都被同等对待,这将更加一致。

2018 年 3 月会议记录:

CWG 同意建议的方向。

关键思想是,对象的生命周期是在其销毁时结束还是在其内存被释放时结束,可能会影响程序的语义。在示例中,

  1. 如果n的生命周期以n的销毁结束,则程序未定义;

  2. 如果n的生命周期结束直到内存被释放,则程序已定义行为1

因此,需要进一步讨论以确定对象的生命周期何时结束。


1 这是因为Core Language Issue 2115:

章节: 9.6 [stmt.jump] 状态: 起草提交者: Richard Smith 日期: 2015- 04-16

标准没有规定退出块时销毁自动变量和释放变量存储之间的相对顺序:所有析构函数是先执行然后释放存储,还是交错?

2016 年 2 月会议记录:

CWG 同意存储应该持续到所有销毁完成为止,尽管“as-if”规则将允许对这种排序进行不可观察的优化。

意图是自动变量的内存释放发生在所有销毁完成后。

【讨论】:

  • 是否真的需要普通旧数据对象拥有与它们占用的存储不同的生命周期?如果目标是允许与别名相关的优化,那么简单地说看似不相关的左值的加载和存储通常是无序的,并指定必须将 lvaues 识别为相关的情况或对它们进行操作,这似乎既简单又有效识别为已排序。
  • @supercat:“是否真的需要纯旧数据对象的生命周期与其占用的存储空间分开?”是的。标量寿命分析可以捕捉到使用未初始化内存和其他类似错误的人。但这要求标量 具有 生命周期,您可以指向一段内存并准确知道其中存在的(嵌套集)对象。
  • 明确一点:这意味着 CWG 已经同意此代码 sn-p 应该具有未定义的行为,并且应该更新标准以使其如此?跨度>
  • @NicolBolas:有明确的指令要么说某个存储区域应该被视为持有 Indeterminate Value,要么应该将存储区域中持有 Indeterminate Value 的任何字节更改为持有 Unspecified值,对于诊断和优化而言,似乎比尝试为此目的使用生命周期规则更有用。
  • @supercat:但是有“显式指令”可以做到这一点。它们被称为“破坏对象”和“创建对象而不初始化它”。它们已经存在并且已经具有有用的语义含义。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-12
  • 2016-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多