生存期
每个对象和引用都有生存期(lifetime),它是一个运行时属性:对于任何对象或引用,在程序执行中都有其生存期开始的一个点,和其结束的一刻。
对象的生存期始于
- 若对象是 union 成员或其子对象,则其生存期仅当该联合体成员是联合体中被初始化的成员,或它被设为活跃时才开始,
- 若对象嵌套于联合体对象中,其生存期可能在包含它的联合体对象被平凡特殊成员函数所赋值或构造时开始,
- 数组对象的生存期也可能在它被 std::allocator::allocate 分配时开始。
某些操作会在给定的存储区域中隐式创建隐式生存期类型的对象并开始其生存期。如果一个隐式创建的对象的子对象不是隐式生存期类型,则其生存期不会隐式开始。
对象的生存期终止于
对象的生存期等于其存储的生存期或在其存储的生存期之内,见存储期。
引用的生存期在其初始化完成时开始,并如同标量对象一样结束。
注意:被引用对象的生存期可能在引用的生存期结束前就终止了,这使得出现悬垂引用成为可能。
非静态数据成员和基类子对象的生存期遵循类初始化顺序开始和结束。
目录 |
[编辑] 临时对象的生存期
当一个纯右值被实质化以便用作一个泛左值时,会创建临时对象,这发生在(C++17 起)下列情形
|
(C++11 起) |
|
(C++17 前) | ||
临时对象的实质化通常会尽可能地延迟,以避免创建不必要的临时对象:见复制消除。 |
(C++17 起) |
当一个类型为
授予此自由度是为了允许对象在寄存器中传递给函数或从函数返回。 |
(C++17 起) |
所有临时对象都在对(词法上)包含其创建点的完整表达式求值的最后一步被销毁,并且如果创建了多个临时对象,它们将以与创建顺序相反的顺序被销毁。即使求值以抛出异常结束,这一点也成立。
对此有以下例外情况
- 临时对象的生存期可以通过绑定到引用来延长,详见引用初始化。
- 在求值用于初始化或复制数组元素的默认或复制构造函数的默认实参时创建的临时对象的生存期,在数组的下一个元素开始初始化之前结束。
|
(C++17 起) |
|
(C++23 起) |
[编辑] 存储重用
如果对象是可平凡析构的,程序不需要调用其析构函数来结束其生存期(注意程序的正确行为可能依赖于该析构函数)。然而,如果程序显式地结束了一个非平凡析构的对象的生存期,该对象是一个变量,那么它必须确保在析构函数可能被隐式调用之前,在原地构造一个相同类型的新对象(例如通过放置 new),即,对于自动对象是由于作用域退出或异常,对于线程局部对象是由于线程退出(C++11 起),或对于静态对象是由于程序退出;否则行为是未定义的。
class T {}; // trivial struct B { ~B() {} // non-trivial }; void x() { long long n; // automatic, trivial new (&n) double(3.14); // reuse with a different type okay } // okay void h() { B b; // automatic non-trivially destructible b.~B(); // end lifetime (not required, since no side-effects) new (&b) T; // wrong type: okay until the destructor is called } // destructor is called: undefined behavior
重用一个曾被静态、线程局部(C++11 起)或自动存储期的 const 完整对象所占据的存储,是未定义行为,因为这类对象可能存储在只读内存中。
struct B { B(); // non-trivial ~B(); // non-trivial }; const B b; // const static void h() { b.~B(); // end the lifetime of b new (const_cast<B*>(&b)) const B; // undefined behavior: attempted reuse of a const }
在求值 new 表达式时,存储在从分配函数返回之后,但在 new 表达式的 initializer 求值之前,被认为是重用了。
struct S { int m; }; void f() { S x{1}; new(&x) S(x.m); // undefined behavior: the storage is reused }
如果在一个曾被另一个对象占据的地址上创建了一个新对象,那么所有指向、引用原对象的指针、引用和名称将自动指代新对象,并且一旦新对象的生存期开始,就可以用来操作新对象,但前提是原对象能被新对象透明地替换。
如果满足以下所有条件,对象 x 可被对象 y 透明地替换
- y 的存储完全覆盖 x 所占据的存储位置。
- y 与 x 的类型相同(忽略顶层 cv 限定符)。
- x 不是一个 const 完整对象。
- x 和 y 都不是基类子对象,或用
[[no_unique_address]]
声明的成员子对象(C++20 起)。 - 满足以下条件之一
- x 和 y 都是完整对象。
- x 和 y 分别是对象 ox 和 oy 的直接子对象,并且 ox 可被 oy 透明地替换。
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
如果不满足上述条件,仍然可以通过应用指针优化屏障 std::launder 来获得一个指向新对象的有效指针。 struct A { virtual int transmogrify(); }; struct B : A { int transmogrify() override { ::new(this) A; return 2; } }; inline int A::transmogrify() { ::new(this) B; return 1; } void test() { A i; int n = i.transmogrify(); // int m = i.transmogrify(); // undefined behavior: // the new A object is a base subobject, while the old one is a complete object int m = std::launder(&i)->transmogrify(); // OK assert(m + n == 3); } |
(C++17 起) |
类似地,如果在类成员或数组成员的存储中创建了一个对象,则创建的对象仅当以下情况才是原对象所在对象的子对象(成员或元素)
- 所在对象的生存期已经开始且未结束
- 新对象的存储完全覆盖原对象的存储
- 新对象与原对象的类型相同(忽略 cv 限定)。
否则,不使用 std::launder 就不能用原子对象的名称来访问新对象。
|
(C++17 起) |
[编辑] 提供存储
作为一种特殊情况,对象可以在 unsigned char 或 std::byte(C++17 起) 的数组中创建(这种情况下称该数组为对象提供存储),如果
- 数组的生存期已经开始且未结束
- 新对象的存储完全位于数组之内
- 数组内没有嵌套的数组对象满足这些约束。
如果数组的那部分先前为另一个对象提供了存储,则该对象的生存期因其存储被重用而结束,但数组本身的生存期不结束(其存储不被认为被重用了)。
template<typename... T> struct AlignedUnion { alignas(T...) unsigned char data[max(sizeof(T)...)]; }; int f() { AlignedUnion<int, char> au; int *p = new (au.data) int; // OK, au.data provides storage char *c = new (au.data) char(); // OK, ends lifetime of *p char *d = new (au.data + 1) char(); return *c + *d; // OK }
[编辑] 生存期外访问
在对象的生存期开始之前,但在将要被该对象占据的存储已被分配之后;或者,在对象的生存期结束之后,但在该对象所占据的存储被重用或释放之前,以下使用标识该对象的泛左值表达式的行为是未定义的,除非对象正在被构造或析构(适用另一套规则)
- 左值到右值的转换(例如,调用一个接受值的函数)。
- 访问非静态数据成员或调用非静态成员函数。
- 将引用绑定到虚基类子对象。
-
dynamic_cast
或typeid
表达式。
以上规则也适用于指针(将引用绑定到虚基类被替换为到虚基类指针的隐式转换),并有两条附加规则
- 对没有对象的存储的指针进行
static_cast
仅在转换为(可能 cv 限定的)void* 时被允许。 - 被转换为可能 cv 限定的 void* 的、指向没有对象的存储的指针,只能被
static_cast
为指向可能 cv 限定的 char 或可能 cv 限定的 unsigned char,或可能 cv 限定的 std::byte(C++17 起) 的指针。
在构造和析构期间,通常允许调用非静态成员函数、访问非静态数据成员,以及使用 typeid
和 dynamic_cast
。然而,因为生存期要么尚未开始(在构造期间)要么已经结束(在析构期间),只有特定的操作是允许的。关于一个限制,请参见构造和析构期间的虚函数调用。
[编辑] 注意
在 CWG 问题 2256 解决之前,非类对象(存储期结束)和类对象(构造的逆序)的生存期结束规则是不同的。
struct A { int* p; ~A() { std::cout << *p; } // undefined behavior since CWG2256: n does not outlive a // well-defined until CWG2256: prints 123 }; void f() { A a; int n = 123; // if n did not outlive a, this could have been optimized out (dead store) a.p = &n; }
在 RU007 解决之前,一个 const 限定类型或引用类型的非静态成员会阻止其所在对象被透明地替换,这使得 std::vector 和 std::deque 难以实现。
struct X { const int n; }; union U { X x; float f; }; void tong() { U u = { {1} }; u.f = 5.f; // OK: creates new subobject of 'u' X *p = new (&u.x) X {2}; // OK: creates new subobject of 'u' assert(p->n == 2); // OK assert(u.x.n == 2); // undefined until RU007: // 'u.x' does not name the new subobject assert(*std::launder(&u.x.n) == 2); // OK even until RU007 }
[编辑] 缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 119 | C++98 | 具有非平凡构造函数的类类型的对象可以 仅在构造函数调用完成后才开始其生存期 |
生存期也开始了 对于其他初始化 |
CWG 201 | C++98 | 默认构造函数的默认实参中的临时对象的生存期 被要求在数组初始化完成时结束 当数组的初始化完成时 |
生存期结束于 初始化下一个 元素之前(也解决了 CWG 问题 124)) |
CWG 274 | C++98 | 一个指定生存期外对象的左值可以被 用作 static_cast 的操作数,仅当转换 最终为 cv 非限定的 char& 或 unsigned char& |
cv 限定的 char& 和 unsigned char& 也允许 |
CWG 597 | C++98 | 以下行为是未定义的 1. 一个指向生存期外对象的指针被隐式 转换为指向非虚基类的指针 2. 一个指代生存期外对象的左值 被绑定到对非虚基类的引用 3. 一个指代生存期外对象的左值被用作 static_cast 的操作数(除少数例外) |
已明确定义 |
CWG 2012 | C++98 | 引用的生存期被指定为与存储期匹配, 要求 extern 引用在其初始化器运行前就是存活的 |
生存期始于 初始化时 |
CWG 2107 | C++98 | CWG 问题 124 的解决方案未应用于复制构造函数 | 已应用 |
CWG 2256 | C++98 | 可平凡析构对象的生存期与其他对象不一致 | 已使其保持一致 |
CWG 2470 | C++98 | 多于一个数组可以为同一对象提供存储 | 只有一个提供 |
CWG 2489 | C++98 | char[] 不能提供存储,但对象 可以在其存储内被隐式创建 |
对象不能被 隐式创建于 char[] 的存储内 |
CWG 2527 | C++98 | 如果因为重用存储而未调用析构函数,且 程序依赖于其副作用,则行为是未定义的 |
这种情况下行为是 良定义的 |
CWG 2721 | C++98 | 对于放置 new,存储重用的确切时间点不明确 | 已明确 |
CWG 2849 | C++23 | 函数形参对象被认为是临时 对象,适用于基于范围的 for 循环的临时对象生存期延长 |
不被视为 临时对象 |
CWG 2854 | C++98 | 异常对象是临时对象 | 它们没有 临时对象 |
CWG 2867 | C++17 | 在结构化绑定声明中创建的临时对象的 生存期未被延长 |
延长至声明 的末尾 |
P0137R1 | C++98 | 在 unsigned char 数组中创建对象会重用其存储 | 其存储未被重用 |
P0593R6 | C++98 | 伪析构函数调用没有效果 | 它会销毁对象 |
P1971R0 | C++98 | 一个 const 限定类型或引用类型的非静态数据成员 会阻止其所在对象被透明地替换 |
限制被移除 |
P2103R0 | C++98 | 透明可替换性不要求保持原始结构 | 要求 |
[编辑] 引用
- C++23 标准 (ISO/IEC 14882:2024)
- 6.7.3 对象生存期 [basic.life]
- 11.9.5 构造与析构 [class.cdtor]
- C++20 标准 (ISO/IEC 14882:2020)
- 6.7.3 对象生存期 [basic.life]
- 11.10.4 构造与析构 [class.cdtor]
- C++17 标准 (ISO/IEC 14882:2017)
- 6.8 对象生存期 [basic.life]
- 15.7 构造与析构 [class.cdtor]
- C++14 标准 (ISO/IEC 14882:2014)
- 3 对象生存期 [basic.life]
- 12.7 构造与析构 [class.cdtor]
- C++11 标准 (ISO/IEC 14882:2011)
- 3.8 对象生存期 [basic.life]
- 12.7 构造与析构 [class.cdtor]
- C++03 标准 (ISO/IEC 14882:2003)
- 3.8 对象生存期 [basic.life]
- 12.7 构造与析构 [class.cdtor]
- C++98 标准 (ISO/IEC 14882:1998)
- 3.8 对象生存期 [basic.life]
- 12.7 构造与析构 [class.cdtor]
[编辑] 参见
C 文档中有关生存期的内容
|