解構子
解構子是一種特殊的成員函式,會在物件生命週期結束時被呼叫。解構子的目的是釋放物件在其生命週期內可能已獲取的資源。
|
解構子不能是協程 (coroutine)。 |
(自 C++20 起) |
[編輯] 語法
解構子(C++20 前)預期解構子(C++20 起) 是使用以下形式的成員函式宣告子來宣告:
class-name-with-tilde ( parameter-list (可選) ) except (可選) attr (可選) |
|||||||||
| class-name-with-tilde | - | 一個識別字表達式,後面可能接有屬性列表,且(C++11 起)可能由一對括號包圍 | ||||||
| 參數列表 | - | 參數列表(必須為空或是 void) | ||||||
| except | - |
| ||||||
| 屬性 | - | (C++11 起) 一個屬性列表。 |
在預期(C++20 起)解構子宣告的宣告說明符中,唯一允許的說明符是constexpr、(C++11 起) friend、 inline 和 virtual(特別注意,不允許指定回傳型別)。
class-name-with-tilde 的識別字表達式必須具備以下其中一種形式:
- 在屬於類別或類別樣板的成員規範中,且非友元宣告 (friend declaration) 的成員宣告中:
- 對於類別,識別字表達式為 ~ 後接緊鄰外層類別的注入類別名稱 (injected-class-name)。
- 對於類別樣板,識別字表達式為 ~ 後接命名當前實例化 (current instantiation) 的類別名稱(C++20 前)緊鄰外層類別樣板的注入類別名稱(C++20 起)。
- 否則,該識別字表達式為限定識別字 (qualified identifier),其結尾的未限定識別字為 ~ 後接由限定識別字非終端部分所指定的類別的注入類別名稱。
[編輯] 說明
當物件的生命週期結束時,解構子會被隱式呼叫,這包含:
- 程式終止時,針對具有靜態儲存期 (storage duration) 的物件。
|
(C++11 起) |
- 作用域結束時,針對具有自動儲存期的物件,以及其生命週期因綁定到參考而被延長的暫存物件。
- delete 表達式,針對具有動態儲存期的物件。
- 完整表達式結束時,針對無名的暫存物件。
- 堆疊展開 (stack unwinding),當例外未被捕捉並逃離其區塊時,針對具有自動儲存期的物件。
解構子也可以被顯式呼叫。
預期解構子 (Prospective destructor)一個類別可以有一個或多個預期解構子,其中一個會被選為該類別的解構子。 為了確定哪個預期解構子是真正的解構子,在類別定義結束時,會針對該類別中參數列表為空的預期解構子進行重載決議 (overload resolution)。如果重載決議失敗,則程式格式錯誤。解構子的選擇不會對被選中的解構子進行 ODR 使用 (odr-use),且該被選中的解構子可能會被刪除。 所有預期解構子都是特殊成員函式。若類別 執行此程式碼 #include <cstdio> #include <type_traits> template<typename T> struct A { ~A() requires std::is_integral_v<T> { std::puts("~A, T is integral"); } ~A() requires std::is_pointer_v<T> { std::puts("~A, T is a pointer"); } ~A() { std::puts("~A, T is anything else"); } }; int main() { A<int> a; A<int*> b; A<float> c; } 輸出 ~A, T is anything else ~A, T is a pointer ~A, T is integral |
(自 C++20 起) |
[編輯] 潛在呼叫解構子
類別 T 的解構子在以下情況下被「潛在呼叫」:
- 被顯式或隱式呼叫時。
- new 表達式建立一個
T型別的物件陣列時。 - return 敘述的結果物件為
T型別時。 - 陣列正在進行聚合初始化 (aggregate initialization),且其元素型別為
T時。 - 類別物件正在進行聚合初始化,且其成員型別為
T(且T非匿名聯集型別)時。 - 潛在構造子物件 (potentially constructed subobject) 為
T型別,且位於非委託(C++11 起)建構子中時。 - 建構一個
T型別的例外物件時。
如果一個潛在呼叫解構子被刪除,或(C++11 起)在呼叫上下文中無法存取,則程式格式錯誤。
[編輯] 隱式宣告解構子
如果類別型別沒有提供使用者宣告的預期(C++20 起)解構子,編譯器將始終宣告一個 inline public 成員解構子。
與任何隱式宣告的特殊成員函式一樣,隱式宣告解構子的例外規格是不拋出例外的,除非任何潛在構造的基底類別或成員的解構子是潛在拋出異常 (potentially-throwing)(C++17 起)隱式定義會直接呼叫具有不同例外規格的函式(C++17 前)。在實務上,除非類別被其解構子為 noexcept(false) 的基底類別或成員「毒化」,否則隱式解構子皆為 noexcept。
[編輯] 隱式定義解構子
如果隱式宣告的解構子未被刪除,當它被 ODR 使用時,編譯器會隱式定義它(即產生並編譯一個函式主體)。這個隱式定義的解構子擁有一個空的函式主體。
|
如果這滿足 constexpr 解構子(C++23 前)constexpr 函式(C++23 起) 的要求,則生成的解構子為 constexpr。 |
(自 C++20 起) |
刪除的解構子若類別
|
(C++11 起) |
[編輯] 平凡解構子
若類別 T 的解構子滿足以下所有條件,則該解構子是平凡的:
- 該解構子是隱式宣告的(C++11 前)不是由使用者提供的 (user-provided)(C++11 起)。
- 該解構子不是虛擬的。
- 所有直接基底類別都具有平凡解構子。
|
(直到 C++26) |
|
(C++26 起) |
平凡解構子是不執行任何操作的解構子。具有平凡解構子的物件不需要 delete 表達式,並且可以僅透過釋放其儲存空間來銷毀。所有與 C 語言相容的資料型別(POD 型別)都是可平凡銷毀的。
[編輯] 解構順序
對於使用者定義或隱式定義的解構子,在執行完解構子主體並銷毀在主體內分配的任何自動物件後,編譯器會以宣告的相反順序呼叫類別的所有非靜態非變體資料成員的解構子,接著以建構順序的相反順序呼叫所有直接非虛擬基底類別的解構子(這會進而呼叫其成員與基底類別的解構子,依此類推),最後,如果此物件是最衍生類別 (most derived class) 的物件,它會呼叫所有虛擬基底的解構子。
即使直接呼叫解構子(例如 obj.~Foo();),~Foo() 中的 return 敘述也不會立即將控制權返回給呼叫者:它會先呼叫所有成員與基底類別的解構子。
[編輯] 虛擬解構子
透過基底類別指標刪除物件時,若基底類別中的解構子不是虛擬的,會引發未定義行為。
class Base { public: virtual ~Base() {} }; class Derived : public Base {}; Base* b = new Derived; delete b; // safe
一個常見的準則(Guideline)是:基底類別的解構子必須是公開且虛擬的,或是保護且非虛擬的。
[編輯] 純虛擬解構子
一個預期(C++20 起)解構子可以被宣告為純虛擬的,例如在需要成為抽象類別但沒有其他適合函式可宣告為純虛擬的基底類別中。純虛擬解構子必須有一個定義,因為當衍生類別被銷毀時,所有基底類別的解構子都會被呼叫。
class AbstractBase { public: virtual ~AbstractBase() = 0; }; AbstractBase::~AbstractBase() {} class Derived : public AbstractBase {}; // AbstractBase obj; // compiler error Derived obj; // OK
[編輯] 例外處理
與任何其他函式一樣,解構子可能會透過拋出例外而終止(這通常要求顯式宣告為 noexcept(false))(C++11 起)。然而,如果此解構子恰好在堆疊展開過程中被呼叫,則會改為呼叫 std::terminate。
雖然 std::uncaught_exceptions 有時可以用來偵測堆疊展開的進行狀況,但允許任何解構子透過拋出例外來終止通常被視為不良實踐。儘管如此,此功能仍被某些函式庫使用,例如 SOCI 和 Galera 3,它們依賴於在建構暫存物件的完整表達式末尾,讓無名暫存物件的解構子拋出例外。
Library fundamental TS v3 中的 std::experimental::scope_success 可能擁有一個潛在拋出例外之解構子,當作用域正常結束且離開函式拋出例外時,它會拋出例外。
[編輯] 註解
對於普通物件(如區域變數)直接呼叫解構子,會在作用域結束時再次呼叫解構子時引發未定義行為。
在泛型上下文中,解構子呼叫語法可以用於非類別型別的物件;這稱為偽解構子呼叫 (pseudo-destructor call):參見成員存取運算子。
| 特性測試巨集 | 數值 | 標準 | 功能 |
|---|---|---|---|
__cpp_trivial_union |
202502L |
(C++26) | 放寬聯集特殊成員函式的平凡性要求 |
[編輯] 範例
#include <iostream> struct A { int i; A(int num) : i(num) { std::cout << "ctor a" << i << '\n'; } (~A)() // but usually ~A() { std::cout << "dtor a" << i << '\n'; } }; A a0(0); int main() { A a1(1); A* p; { // nested scope A a2(2); p = new A(3); } // a2 out of scope delete p; // calls the destructor of a3 }
輸出
ctor a0 ctor a1 ctor a2 ctor a3 dtor a2 dtor a3 dtor a1 dtor a0
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯應用於之前的 C++ 標準。
| DR | 應用於 | 出版時的行為 | 正確的行為 |
|---|---|---|---|
| CWG 193 | C++98 | 解構子內的自動物件是否在 類別的基底類別和成員子物件銷毀之前或之後銷毀 當時是不明確的 |
規定它們應在銷毀 這些子物件 之前銷毀 |
| CWG 344 | C++98 | 解構子的宣告子語法有缺陷(與 CWG 問題 194 和 CWG 問題 263 有相同問題) |
將語法變更為專用的 函式宣告子語法 |
| CWG 1241 | C++98 | 靜態成員可能在 解構子執行後立即被銷毀 |
僅銷毀非 靜態成員 |
| CWG 1353 | C++98 | 隱式宣告解構子未定義的條件 沒有考慮多維陣列型別 |
考量這些型別 |
| CWG 1435 | C++98 | 解構子宣告語法中 “類別名稱”的含義不明確 |
將語法變更為專用的 函式宣告子語法 |
| CWG 2180 | C++98 | 非最衍生類別的類別之解構子 會呼叫其虛擬直接基底類別的解構子 |
修正為不會呼叫那些解構子 |
| CWG 2807 | C++20 | 宣告說明符可能包含 consteval | 禁止 |