生存期
每个对象和引用都有生存期 (lifetime) ,这是一项运行时性质:每个对象或引用在程序执行时都存在一个时刻开始其生存期,也都存在一个时刻结束其生存期。
对象的生存期始于:
- 若该对象是联合体成员或其子对象,则其生存期在该联合体成员是联合体中的被初始化成员,或它被设为活跃才开始,或者
- 若该对象内嵌于联合体对象,则其生存期开始于平凡特殊成员函数赋值或构造含有它的联合体对象,或者
- 数组对象的生存期可以因为该对象被 std::allocator::allocate 分配而开始。
某些操作在给定的存储区域中隐式创建对象,并开始其生存期,这些对象拥有隐式生存期类型(见后述)。
对象的生存期结束于:
- 若它拥有非类类型,则为销毁该对象时(可能经由伪析构函数调用),或者
- 若它拥有类类型,则为析构函数调用开始时,或者
- 该对象所占据的存储被释放,或被不内嵌于它的对象所重用。
对象的生存期与它的存储的生存期相同,或者内嵌于其中,参见存储期。
引用的生存期,从其初始化完成之时开始,并与标量对象以相同的方式结束。
注意:被引用对象的生存期可能在引用的生存期结束之前就会结束,这会造成悬垂引用。
非静态数据成员和基类子对象的生存期,按照类初始化顺序开始和结束。
隐式生存期类型
一个对象类型被称为隐式生存期类型,若它是:
- 标量类型,或
- 数组类型,或
- 聚合类类型,或
- 拥有下列成员的类类型
- 至少一个平凡的合格构造函数,以及
- 一个平凡且未被删除的析构函数,
- 或上述类型之一的 cv 限定版本。
注意若隐式创建的对象的子对象不拥有隐式生存期类型,则其生存期不会隐式开始。
临时对象的生存期
在下列情况中进行纯右值的实质化,从而能将它作为泛左值使用,即 (C++17 起)创建临时对象:
|
(C++11 起) |
|
(C++17 前) | ||
(C++17 起) |
所有临时对象的销毁都是在(词法上)包含创建它的位置的全表达式的求值过程的最后一步进行的,而当创建了多个临时对象时,它们是以其创建的相反顺序销毁的。即便求值过程以抛出异常而终止也是如此。
对此有两种例外情况:
- 可以通过绑定到 const 左值引用或右值引用 (C++11 起)来延长临时对象的生存期,细节见引用初始化。
- 在对数组的某个元素使用含有默认实参的默认或复制构造函数进行初始化时,对该默认实参求值所创建的临时对象的生存期将在该数组的下一个元素的初始化开始之前终止。
存储的重用
如果对象可平凡析构,或者程序并不依赖析构函数中的副作用的话,程序不必调用该对象的析构函数。然而如果程序显式终止非可平凡析构对象的生存期的话,它必须确保在可能隐式地调用析构函数前(例如对于自动对象是由于退出作用域或发生异常,对于线程局部对象是由于线程退出,或对于静态对象是由于程序退出),原位构造(比如使用布置 new )一个新的同类型对象;否则行为未定义。
class T {}; // 平凡 struct B { ~B() {} // 非平凡 }; void x() { long long n; // 自动、平凡 new (&n) double(3.14); // 以不同的类型进行重用没有问题 } // OK void h() { B b; // 自动的非可平凡析构对象 b.~B(); // 生存期结束(不必要,因为没有副作用) new (&b) T; // 类型错误:直到析构函数被调用之前都没问题 } // 调用了析构函数:未定义行为
重用某个具有静态、线程局部或者自动存储期的 const 完整对象所占据的存储是未定义行为,因为这种对象可能被存储于只读内存中。
struct B { B(); // 非平凡 ~B(); // 非平凡 }; const B b; // const 静态对象 void h() { b.~B(); // b 的生存期结束 new (const_cast<B*>(&b)) const B; // 未定义行为:试图重用 const 对象 }
一旦在某个对象所曾占据的地址上创建了新的对象,所有原对象的指针、引用及名字都会自动代表新的对象,而且一旦新对象的生存期开始,它们就可以用于操作这个新对象,但仅当新对象可透明替换旧对象才可以。
对象 y
可透明替换对象 x
,若:
-
y
的存储准确地与x
所占据的存储位置重叠 -
y
与x
拥有相同类型(忽略顶层 cv 限定符) -
x
不是完整 const 对象 -
x
与y
均不是基类子对象,或以[[no_unique_address]]
声明的成员子对象 (C++20 起) - 要么
-
x
与y
都是完整对象,或 -
x
与y
分别为对象ox
与oy
的直接子对象,而oy
可透明替换ox
。
-
struct C { int i; void f(); const C& operator=( const C& ); }; const C& C::operator=( const C& other) { if ( this != &other ) { this->~C(); // *this 的生存期结束 new (this) C(other); // 创建了 C 类型的新对象 f(); // 定义明确的 } return *this; } C c1; C c2; c1 = c2; // 定义明确的 c1.f(); // 定义明确的;c1 代表 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(); // 未定义行为:新的 A 对象是基类子对象,而旧的是完整对象 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[maxv(sizeof(T)...)]; }; int f() { AlignedUnion<int, char> au; int *p = new (au.data) int; // OK : au.data 提供存储 char *c = new (au.data) char(); // OK : *p 的生存期结束 char *d = new (au.data + 1) char(); return *c + *d; // OK }
在生存期之外进行访问
在对象的生存期开始之前但其存储将要占据的存储已经分配之后,或者在对象的生存期已经结束之后但其所曾占据的存储被重用或释放之前,对代表这个对象的泛左值表达式的以下这些用法是未定义的:
- 左值向右值转换(比如对接受其值的函数进行调用),除非正在构造或析构该对象(此时应用不同的规则集合):
- 访问其非静态数据成员或调用非静态成员函数。
- 绑定引用到其某个虚基类子对象。
-
dynamic_cast
或typeid
表达式。
以上规则也适用于指针(绑定引用到虚基类改为隐式转换为虚基类的指针),并有两条额外的规则:
- 对指向没有对象的存储的指针进行
static_cast
时只允许将其强制转换为(可能 cv 限定的)void*。 - 转型到 void* 的指向无对象存储的指针,只能被
static_cast
到指向可能 cv 限定的 char 、或可能 cv 限定的 unsigned char 、或可能 cv 限定的 std::byte (C++17 起) 的指针。
在构造和析构的过程中,通常允许调用非静态成员函数,访问非静态数据成员,以及使用 typeid
和 dynamic_cast
。然而,因为生存期尚未开始(在构造期间)或已结束(在析构期间),只允许特定的操作。其他限制条件参见在构造和析构过程中调用虚函数。
注解
核心问题 2256 解决前,非类对象(存储期的终止)和类对象(按构造顺序的逆序)的生存期终止规则存在差别:
struct A { int* p; ~A() { std::cout << *p; } // CWG2256 起为未定义行为: n 不活到 a 的生存期之后 // CWG2256 前有恰当定义:打印 123 }; void f() { A a; int n = 123; // 假如 n 不活到 a 的生存期之后,则能把这条语句优化掉(死存储) 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 :创建 'u' 的新子对象 X *p = new (&u.x) X {2}; // OK :创建 'u' 的新子对象 assert(p->n == 2); // OK assert(u.x.n == 2); // RU007 前未定义: // 'u.x' 不指名新子对象 assert(*std::launder(&u.x.n) == 2); // 即使在 RU007 前也 OK }
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 201 | C++98 | 要求默认构造函数的默认实参中的临时对象的生存期在数组初始化完成时结束 | 生存期在下个元素初始化之前结束 |
CWG 2012 | C++98 | 引用的生存期被指定为与存储期匹配,这要求 extern 引用在其初始化器运行前已存活 | 生存期始于初始化 |
CWG 2107 | C++98 | CWG 124 的解决方案未应用到复制构造函数 | 已应用 |
P0137R1 | C++98 | 在 unsigned char 数组中创建对象重用其存储 | 其存储不被重用 |
CWG 2256 | C++98 | 可平凡析构对象的生存期与其他对象不一致 | 使之一致 |
P1971R0 | C++98 | const 限定类型或引用类型的非静态数据成员使包含它的对象不能为可透明替换 | 移除限制 |
P2103R0 | C++98 | 可透明替换性不要求保持原结构 | 要求 |
P0593R6 | C++98 | 伪析构函数调用无效果 | 它销毁对象 |
CWG 2470 | C++98 | 可以有多于一个数组为同一对象提供存储 | 仅一个提供 |
引用
- C++20 标准(ISO/IEC 14882:2020):
- 6.7.3 Object lifetime [basic.life]
- 11.10.4 Construction and destruction [class.cdtor]
- C++17 标准(ISO/IEC 14882:2017):
- 6.8 Object lifetime [basic.life]
- 15.7 Construction and destruction [class.cdtor]
- C++14 标准(ISO/IEC 14882:2014):
- 3 Object lifetime [basic.life]
- 12.7 Construction and destruction [class.cdtor]
- C++11 标准(ISO/IEC 14882:2011):
- 3.8 Object lifetime [basic.life]
- 12.7 Construction and destruction [class.cdtor]
- C++03 标准(ISO/IEC 14882:2003):
- 3.8 Object lifetime [basic.life]
- 12.7 Construction and destruction [class.cdtor]
- C++98 标准(ISO/IEC 14882:1998):
- 3.8 Object lifetime [basic.life]
- 12.7 Construction and destruction [class.cdtor]