生存期
每個物件和引用都有生存期(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 文件中有關生存期的內容
|