生存期

来自cppreference.com
< cpp‎ | language

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

  • 对于类或者聚合类型的对象,如果以平凡默认构造函数以外的方式初始化它,或任何其子对象,那么其生存期就从初始化结束之时开始。
  • 对于具有非平凡的析构函数的类类型的对象来说,其生存期在析构函数开始执行的时刻结束。
  • 联合体的成员的生存期,从使得该成员活跃时开始。
  • 所有的其他对象(以平凡默认构造函数初始化的类对象,非类类型的对象,它们的数组,等等)的生存期,开始于为该对象分配正确对齐的存储之时,结束于存储被解分配或为其他对象所重用之时。

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

引用的生存期就是其存储期。

(C++14 前)
引用的生存期,从其初始化完成之时开始,并与标量对象相同的方式结束。 (C++14 起)

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

成员对象和基类子对象的生存期,按照类初始化顺序开始和结束。

目录

[编辑] 临时对象的生存期

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

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

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

(C++17 起)

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

对此有两种例外情况:

  • 可以通过绑定到 const 左值引用或右值引用 (C++11 起)延长临时对象的生存期,细节见引用初始化
  • 对数组元素初始化时所使用的默认构造函数的默认实参所进行的求值,其所创建的临时对象的生存期,将在数组的下一个元素的初始化开始之前终止。
(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 对象
}

一旦在某个对象所曾占据的地址上创建了新的对象,所有原对象的指针,引用及其名字,都会自动代表新的对象,而且一旦新对象的生存期开始,它们就可以用于操作这个新对象,但只能在满足下列条件的情况下才能这样做:

  • 新对象的存储与原对象层占据的存储位置严格重叠
  • 新对象和原对象(忽略顶层的 cv 限定符)具有相同的类型
  • 原对象的类型非 const 限定
  • 如果原对象具有类类型,则它不能含有任何 const 限定的类型或引用类型的非静态数据成员
  • 原对象曾为 T 类型的最终派生对象,且新对象也是 T 类型的最终派生对象(就是说,它们都不是基类子对象)。
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 来获得指向新对象的有效指针。

相似地,当在类成员或数组元素中创建对象时,只有满足如下条件,所创建的对象才是包含原对象的对象的子对象(成员或元素):

  • 包含对象的生存期已经开始且尚未结束
  • 新对象的存储于原对象的存储严格重叠
  • 新对象和原对象(忽略 cv 限定性)具有相同的类型。

否则(比如子对象含有引用成员 const 子对象),不使用 std::launder 就不能以原对象的名字访问新对象:

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(*std::launder(&u.x.n) == 2); // OK
  assert(u.x.n == 2);                 // 未定义: 'u.x' 不指名新的子对象
}

一种特殊情况是,以下条件下可以在 unsigned charstd::byte 的数组中创建对象(这种情况下称这个数组为对象提供存储):

  • 数组的生存期已经开始且尚未结束
  • 新对象的存储完全适于数组之内
  • 不存在满足这些制约的更小的数组对象。

如果该数组的这个部分之前曾为另一个对象提供存储的话,那个对象的生存期就会结束,因为它的存储被重用了,不过数组自身的生存期并未结束(其存储并不被当成是被重用了)。

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
}
(C++17 起)

[编辑] 在生存期之外进行访问

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

  1. 左值向右值转换(比如对接受其值的函数进行调用)。
  2. 访问其非静态数据成员或调用非静态成员函数。
  3. 绑定引用到其某个虚基类子对象。
  4. dynamic_casttypeid 表达式。

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

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

在构造和析构的过程中,还有其他的限制条件,参见在构造和析构过程中进行虚函数调用

[编辑] 注意

非类对象和类对象在生存期终止方面的规则的差别(存储期的终止和与构造相反顺序),在以下例子中有所体现:

struct A {
  int* p;
  ~A() { std::cout << *p; } // 如果 n 比 a 活得长,将会打印 123
};
void f() {
  A a;
  int n = 123; // 如果 n 不比 a 活得长,则这会被优化掉(死存储)
  a.p = &n;
}

[编辑] 缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 2012 C++14 引用的生存期被指定为与存储期匹配,这要求 extern 引用在其初始化器运行前已存活 生存期始于初始化

[编辑] 参阅

生存期C 文档