生存期
每个对象和引用都有生存期,这是一个运行时属性:对于任何对象或引用,程序执行时都有其生存期开始的点,以及其结束的时刻。
对象的生存期在以下情况下开始:
- 如果对象是联合体成员或其子对象,则只有在该联合体成员是联合体中已初始化的成员,或使其变为活动成员时,其生存期才开始,
- 如果对象嵌套在联合体对象中,则如果包含的联合体对象通过平凡特殊成员函数赋值或构造,则其生存期可能开始,
- 如果数组对象是由std::allocator::allocate分配的,则其生存期也可能开始。
某些操作隐式创建对象隐式生存期类型在给定的存储区域中并开始其生存期。如果隐式创建的对象的子对象不是隐式生存期类型,则其生存期不会隐式开始。
对象的生存期在以下情况下结束:
对象的生存期等于或嵌套在其存储的生存期内,请参见存储期。
引用的生存期在其初始化完成时开始,并像标量对象一样结束。
注意:被引用对象的生存期可能在引用的生存期结束之前结束,这使得悬垂引用成为可能。
非静态数据成员和基类子对象的生存期按照类初始化顺序开始和结束。
目录 |
[编辑] 临时对象生存期
临时对象在当一个纯右值被具现化,以便可以将其用作泛左值时创建,这发生在(自 C++17 起)以下情况中
|
(自 C++11 起) |
|
(直到 C++17) | ||
临时对象的具现化通常会尽可能延迟,以避免创建不必要的临时对象:请参见复制省略。 |
(自 C++17 起) |
当类型
授予此自由度是为了允许对象在寄存器中传递给函数或从函数返回。 |
(自 C++17 起) |
所有临时对象都在求值完整表达式的最后一步被销毁,该完整表达式(词法上)包含它们被创建的点,并且如果创建了多个临时对象,则它们以与创建顺序相反的顺序被销毁。即使该求值以抛出异常结束,也是如此。
以下是例外情况
- 临时对象的生存期可以通过绑定到引用来延长,有关详细信息,请参见引用初始化。
- 用于初始化或复制数组元素的默认或复制构造函数的默认参数求值时创建的临时对象的生存期在数组的下一个元素开始初始化之前结束。
|
(自 C++17 起) |
|
(自 C++23 起) |
[编辑] 存储重用
如果对象是可平凡析构的,则程序不需要调用对象的析构函数来结束其生存期(请注意,程序的正确行为可能取决于析构函数)。但是,如果程序显式结束了非常量可析构对象的生存期,则必须确保在隐式调用析构函数之前,即由于自动对象的范围退出或异常,由于线程局部对象的线程退出,(自 C++11 起)或由于静态对象的程序退出,在原位置构造相同类型的新对象(例如,通过放置 new);否则行为是未定义的。
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 表达式时,存储在从分配函数返回后,但在求值新表达式的初始化器之前被视为重用
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
表达式。
上述规则也适用于指针(将引用绑定到虚基类被替换为隐式转换为指向虚基类的指针),但有两个附加规则
- 只有在转换为(可能带有 cv 限定的)void* 时,才允许
static_cast
指向没有对象的存储的指针。 - 指向没有对象的存储的指针,这些指针已转换为可能带有 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++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
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 | 引用的生存期被指定为匹配存储期, 要求外部引用在其初始化器运行之前就存在 |
生存期开始于 初始化时 |
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 文档 关于 生存期
|