生存期

来自cppreference.com
< cpp‎ | language

每个对象引用都有生存期 (lifetime) ,这是一项运行时性质:每个对象或引用在程序执行时都存在一个时刻开始它的生存期,也都存在一个时刻结束它的生存期。

对象的生存期在以下时刻开始:

  • 获得拥有它的类型的正确大小与对齐的存储,并且
  • 完成它的初始化(如果存在)(包括不经由构造函数或经由平凡默认构造函数默认初始化),除非
  • 如果该对象是联合体成员或它的子对象,那么它的生存期在该联合体成员是联合体中的被初始化成员,或它被设为活跃才会开始,或者
  • 如果该对象内嵌于联合体对象,那么它的生存期在平凡特殊成员函数赋值或构造含有它的联合体对象时开始,或者
  • 数组对象的生存期可以因为该对象被 std::allocator::allocate 分配而开始。

某些操作在给定的存储区域中隐式创建具有隐式生存期类型的对象,并开始它的生存期。如果隐式创建的对象的子对象不拥有隐式生存期类型,那么它的生存期不会隐式开始。

对象的生存期在以下时刻结束:

  • 如果该对象是非类类型,那么在销毁该对象时(可能经由伪析构函数调用销毁),或者
  • 如果该对象是类类型,那么在析构函数调用开始时,或者
  • 该对象所占据的存储被释放,或被不内嵌于它的对象所重用时。

对象的生存期与它的存储的生存期相同,或者内嵌于其中,参见存储期

引用的生存期,从它的初始化完成之时开始,并与标量对象以相同的方式结束。

注意:被引用对象的生存期可能在引用的生存期结束之前就会结束,这会造成悬垂引用

非静态数据成员和基类子对象的生存期按照类初始化顺序开始和结束。

临时对象的生存期

在下列情况中进行纯右值的实质化,从而能将它作为泛左值使用,即 (C++17 起)创建临时对象:

(C++11 起)
(C++11 起)
(C++17 前)
(C++17 起)

临时对象的实质化通常会尽可能地被推迟,以免创建不必要的临时对象:参见复制消除

(C++17 起)

所有临时对象的销毁都是在(词法上)包含创建它的位置的完整表达式的求值过程的最后一步进行的,而当创建了多个临时对象时,它们是以被创建的相反顺序销毁的。即便求值过程以抛出异常而终止也是如此。

对此有两种例外情况:

  • 可以通过绑定到引用来延长临时对象的生存期,细节见引用初始化
  • 在对数组的某个元素使用含有默认实参的默认或复制构造函数进行初始化时,对该默认实参求值所创建或复制的临时对象的生存期将在该数组的下一个元素的初始化开始之前终止。

存储的重用

如果对象可以平凡析构,程序不必调用该对象的析构函数就能终止它的生存期(需要注意程序的正确行为可能会依赖该析构函数)。然而如果程序显式终止非可平凡析构对象的生存期的话,它必须确保在可能隐式地调用析构函数前(例如对于自动对象是由于退出作用域或发生异常,对于线程局部对象是由于线程退出,或对于静态对象是由于程序退出),原位构造(比如使用布置 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 所占据的存储位置重叠
  • yx 拥有相同类型(忽略顶层 cv 限定符)
  • x 不是完整的 const 对象
  • xy 都不是基类子对象或以 [[no_unique_address]] 声明的成员子对象 (C++20 起)
  • 要么
  • xy 都是完整对象,或
  • xy 分别是对象 oxoy 的直接子对象,而 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
}

在生存期外进行访问

在对象的生存期开始之前但在它将要占据的存储已经分配之后,或者在对象的生存期已经结束之后但它曾占据的存储被重用或释放之前,对代表这个对象的泛左值表达式的以下这些用法是未定义的:

  1. 左值向右值转换(比如对接受它的值的函数进行调用),除非正在构造或析构该对象(此时适用不同的规则集合):
  2. 访问它的非静态数据成员或调用它的非静态成员函数。
  3. 绑定引用到它的某个虚基类子对象。
  4. dynamic_casttypeid 表达式。

以上规则也适用于指针(绑定引用到虚基类改为隐式转换为虚基类的指针),并有两条额外的规则:

  1. 对指向没有对象的存储的指针进行 static_cast 时只能强制转换到(可有 cv 限定的)void*
  2. 转型到 void* 的指向无对象存储的指针,只能被 static_cast 到指向可有 cv 限定的 char 、或可有 cv 限定的 unsigned char、或可有 cv 限定的 std::byte (C++17 起) 的指针。

在构造和析构的过程中,通常允许调用非静态成员函数,访问非静态数据成员,以及使用 typeiddynamic_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::vectorstd::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++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 119 C++98 带有不平凡的构造函数的类类型的对象的生存期只能在构造函数完成时开始 也能在其他初始化完成时开始
CWG 201 C++98 要求默认构造函数的默认实参中的临时对象的生存期在数组初始化完成时结束 生存期在下个元素初始化之前结束
(同时解决了 CWG 问题 124
CWG 274 C++98 指代生存期外对象的左值只有在最终转换到无 cv 限定的 char&
unsigned char& 时才能作为 static_cast 的操作数
也可以转换到有 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 2523 C++98 如果有构造函数因为重用存储而没有被调用,
并且程序依赖它的副作用,那么行为未定义
此时行为具有良好定义
P0137R1 C++98 unsigned char 数组中创建对象会重用它的存储 它的存储不会被重用
P0593R6 C++98 伪析构函数调用无效果 它会销毁对象
P1971R0 C++98 const 限定类型或引用类型的非静态数据成员使包含它的对象不能为可透明替换 移除限制
P2103R0 C++98 可透明替换性不要求保持原结构 要求

引用

  • C++23 标准(ISO/IEC 14882:2023):
  • 6.7.3 Object lifetime [basic.life]
  • 11.9.5 Construction and destruction [class.cdtor]
  • 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]

参阅