【问题标题】:After an object is destroyed, what happens to subobjects of scalar type?一个对象被销毁后,标量类型的子对象会发生什么?
【发布时间】:2012-07-23 04:03:44
【问题描述】:

考虑这段代码(renewcleanse 的不同值):

struct T {
    int mem;
    T() { }
    ~T() { mem = 42; }
};

// identity functions, 
// but breaks any connexion between input and output
int &cleanse_ref(int &r) {
    int *volatile pv = &r; // could also use cin/cout here
    return *pv;
}

void foo () {
    T t;
    int &ref = t.mem;
    int &ref2 = cleanse ? cleanse_ref(ref) : ref;
    t.~T();
    if (renew)
        new (&t) T;
    assert(ref2 == 42);
    exit(0);
}

assert 是否保证通过?

我了解推荐这种风格。 像“这不是一个正确的做法”这样的意见在这里不感兴趣。

我想要一个显示标准引用的完整逻辑证明的答案。编译器作者的意见也可能很有趣。

编辑:现在将两个问题合二为一!见renew参数(带renew == 0,这是原问题)。

编辑 2:我想我的问题真的是:什么是成员对象?

编辑 3:现在使用另一个 cleanse 参数!

【问题讨论】:

  • 反对票很快... :(
  • 我不明白为什么。这是个有趣的问题。来自我的 +1。
  • 如果您不喜欢这个问题,我建议您将“language-lawyer”添加到您忽略的标签列表中。
  • &p 是什么?没有声明 p
  • 标准不允许您提供证明。在对象生命周期定义方面存在缺陷。

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


【解决方案1】:

我最初有这两个引号,但现在我认为它们实际上只是指定像int &ref = t.mem; 这样的事情必须在t 的生命周期内发生。在您的示例中,它确实如此。

12.7 第 1 段:

对于具有非平凡析构函数的对象,在析构函数完成执行后引用该对象的任何非静态成员或基类会导致未定义的行为。

第 3 段:

要形成指向对象obj 的直接非静态成员(或访问其值)的指针,obj 的构造应该已经开始并且它的销毁应该没有完成,否则计算指针值(或访问成员值)会导致未定义的行为。

我们这里有一个T 类型的完整对象和一个int 类型的成员子对象。

3.8 第 1 段:

T 类型对象的生命周期开始于:

  • 获得了与T 类型正确对齐和大小的存储,并且
  • 如果对象有非平凡的初始化,它的初始化就完成了。

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

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

顺便说一句,3.7.3 p1:

这些[自动存储持续时间]实体的存储将持续到创建它们的块退出。

还有 3.7.5:

成员子对象、基类子对象和数组元素的存储时长为其完整对象的存储时长(1.8)。

因此,在此示例中,无需担心编译器“释放”exit 之前的存储空间。

3.8p2 中的非规范性注释提到“12.6.2 描述了基子对象和成员子对象的生命周期”,但那里的语言只讨论初始化和析构函数,而不是“存储”或“生命周期”,所以我得出结论该部分不影响普通类型子对象的“生命周期”定义。

如果我正确地解释了这一切,当renew 为假时,完整类对象的生命周期在显式析构函数调用结束时结束,但int 子对象的生命周期继续到程序。

3.8 第 5 段和第 6 段说,在任何对象的生命周期之前或之后指向“分配的存储”的指针和引用都可以以有限的方式使用,并列出了很多你可能不会用它们做的事情。左值到右值的转换,就像ref == 42 要求的表达式一样,就是其中之一,但如果int 的生命周期尚未结束,这不是问题。

所以我认为renew false,程序格式正确,assert 成功!

如果renew 为true,则存储被程序“重用”,因此原始int 的生命周期结束,另一个int 的生命周期开始。但随后我们进入 3.8 第 7 段:

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:

  • 新对象的存储完全覆盖了原始对象占用的存储位置,并且
  • 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且
  • 原始对象的类型不是 const 限定的,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且
  • 原始对象是 T 类型的最衍生对象 (1.8),而新对象是 T 类型的最衍生对象(也就是说,它们不是基类子对象)。

这里的第一个要点是最棘手的。对于像T 这样的标准布局类,同一个成员肯定必须始终在同一个存储中。当类型不是标准布局时,我不确定这是否在技术上是必需的。

虽然ref是否仍然可以使用,但这个例子还有一个问题。

12.6.2 第 8 段:

在对类X 的构造函数的调用完成后,如果X 的成员在执行复合语句 的主体的过程中既没有初始化也没有赋值构造函数,成员具有不确定的值。

意味着如果将t.mem 设置为0 或0xDEADBEEF,则实现是合规的(有时调试模式实际上会在调用构造函数之前执行此类操作)。

【讨论】:

  • 所以C++中的对象可以有三种状态:“constructed”(ctor结束),“constructing”(ctor调用,未完成),“notconstructed”(ctor未调用)?这是在哪里解释的?
  • 重新引用 12.6.2 第 8 段,这似乎是一个非规范性注释,我不确定它是否在所有情况下都是正确的。规范语言似乎是“否则,实体被默认初始化”......如果所讨论的实体具有静态存储持续时间,因此之前已经被零初始化,那么后续的默认初始化是否将实体保持为零,还是得到一个不确定的值?
  • @aschepler 如果你有兴趣,我问了一个问题,希望能更好地理解 12.6.2 文本:stackoverflow.com/questions/33456141/…
  • “所以我认为 renew false,程序格式正确并且断言成功!”,但这与您引用的 12.7p3 冲突。显式的析构函数调用破坏了对象,因此如果您随后从引用中读取,它就是 UB(因为您尝试访问被破坏对象的非静态成员的值)。
【解决方案2】:

您没有破坏内存,您只是手动调用了析构函数(在这种情况下,它与调用普通方法没有什么不同)。 t 变量的内存(堆栈部分)未“释放”。因此,此断言将始终与您当前的代码一起通过。

【讨论】:

  • 但考虑assert(t.mem == 42):如果renew 为0,它将使用不存在的对象。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多