我最初有这两个引号,但现在我认为它们实际上只是指定了诸如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
错误,程序格式正确并且assert
成功!
renew
如果为 true,则存储被程序“重用”,因此原始的int
生命周期结束,另一个生命周期int
开始。但随后我们进入 3.8 第 7 段:
如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原始对象占用的存储位置创建一个新对象,一个指向原始对象的指针,一个指向原始对象的引用引用原始对象,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:
- 新对象的存储恰好覆盖了原始对象占用的存储位置,并且
- 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且
- 原始对象的类型不是 const 限定的,并且,如果是类类型,则不包含其类型为 const 限定或引用类型的任何非静态数据成员,并且
- 原始对象是类型最衍生的对象(1.8),
T
而新对象是类型最衍生的对象T
(也就是说,它们不是基类子对象)。
这里的第一个要点是最棘手的。对于像你这样的标准布局类T
,同一个成员当然必须总是在同一个存储中。当类型不是标准布局时,我不确定这是否在技术上是必需的。
尽管是否ref
仍然可以使用,但在这个例子中还有另一个问题。
12.6.2 第 8 段:
在对类的构造函数的调用完成后,如果在构造函数主体的复合语句X
执行期间,成员X
既没有初始化也没有给定值,则该成员具有不确定的值。
这意味着如果它设置t.mem
为零或0xDEADBEEF
(有时调试模式实际上会在调用构造函数之前执行此类操作),则实现是兼容的。