Item 17:理解特殊成員函式的生成規則 (Understand Special Member Function Generation)
Overview Table
特殊成員函式(special member functions, SMF) 是編譯器願意自行生成的成員函式。C++98 有四個(default constructor、destructor、copy constructor、copy assignment operator),C++11 新增兩個:move constructor 與 move assignment operator。生成的 SMF 一律是 public、inline、非 virtual(唯一例外:基底類別 destructor 為 virtual 時,衍生類別生成的 destructor 也是 virtual)。
| 使用者宣告了… | copy ctor | copy assign | move ctor | move assign |
|---|---|---|---|---|
| (什麼都沒宣告) | 生成 | 生成 | 生成 | 生成 |
| copy ctor | — | 生成(已棄用) | 不生成 | 不生成 |
| copy assign | 生成(已棄用) | — | 不生成 | 不生成 |
| move ctor | =delete | =delete | — | 不生成 |
| move assign | =delete | =delete | 不生成 | — |
| destructor | 生成(已棄用) | 生成(已棄用) | 不生成 | 不生成 |
Move 生成三條件(缺一不可):類別中 沒有宣告任何 copy 操作、沒有宣告任何 move 操作、沒有宣告 destructor。三者皆無、且程式碼有用到時,編譯器才生成 move 操作。
六個 SMF 的 C++11 生成規則
| SMF | 生成條件 | 行為與備註 |
|---|---|---|
| default constructor | 類別完全沒有使用者宣告的建構子 | 與 C++98 相同 |
| destructor | 未宣告 destructor | 與 C++98 幾乎相同;預設為 noexcept;基底 dtor 為 virtual 才 virtual |
| copy constructor | 未宣告 copy ctor;宣告了 move 操作則被 delete | memberwise copy;類別有 copy assign 或 dtor 時仍生成但已棄用 (deprecated) |
| copy assignment | 未宣告 copy assign;宣告了 move 操作則被 delete | memberwise copy;類別有 copy ctor 或 dtor 時仍生成但已棄用 |
| move ctor / assign | 無 copy 操作、無 move 操作、無 destructor | memberwise move(含基底類別部分) |
class Widget {
public:
Widget(Widget&& rhs); // move constructor
Widget& operator=(Widget&& rhs); // move assignment operator
};
- 兩個 copy 操作彼此獨立:宣告其中一個,不妨礙編譯器生成另一個(C++98 與 C++11 皆然)。
- 兩個 move 操作彼此不獨立:宣告其中一個,另一個就不會生成——因為自訂 move ctor 暗示 memberwise move 對此類別不正確,那麼 memberwise move assignment 大概也不正確。
「memberwise move」其實是 move 請求(move request):對每個 non-static 資料成員與基底類別套用
std::move,再經 overload resolution 決定實際做 move 還是 copy。不支援 move 的成員(如多數 C++98 舊類別)會改以 copy 完成,程式照樣編譯執行——只是變慢。細節見 Item 23。生成規則的互相影響與 Rule of Three
編譯器是否生成 move operations?
────────────────────────────────
類別宣告了 copy 操作? ──是──▶ move 不生成(move 請求退回 copy)
│ 否
類別宣告了 move 操作? ──是──▶ 另一個 move 不生成;
│ 否 兩個 copy 操作被 =delete
類別宣告了 destructor?──是──▶ move 不生成
│ 否 (copy 仍生成,但已 deprecated)
▼
(需要時)生成 memberwise move
- 宣告 copy 操作 → 抑制 move 生成:memberwise copy 不適用,memberwise move 大概也不適用。
- 宣告 move 操作 → copy 操作被 delete(機制見 Item 11 的
=delete):memberwise move 不對,memberwise copy 也不會對。這不會破壞 C++98 舊碼——舊碼不可能有 move 操作,會加 move 的類別就必須遵守 C++11 規則。 - Rule of Three:宣告了 copy ctor、copy assign、destructor 其中之一,就該三個全部宣告——自訂 copy 幾乎都源自資源管理,而 destructor 通常也參與(釋放資源)。推論:有使用者宣告的 destructor,代表 memberwise copy 很可能不正確 → 這正是 C++11 「有 dtor 就不生成 move」的理論依據。
例外:基於 Rule of Three,「有 dtor 或 copy 操作就不該生成 copy 操作」才合理,但為了不破壞大量舊碼,C++11 仍會生成——只是將此行為標記為 deprecated。依賴這種生成的程式碼應改用
= default 明確化。= default:明確要求預設行為
若編譯器生成的行為正確(memberwise 就是你要的),用 C++11 的 = default 明確聲明。典型場景是多型基底類別:需要 virtual destructor,但宣告 dtor 會抑制 move,宣告 move 又會 delete copy——於是層層 = default 補回來:
class Base {
public:
virtual ~Base() = default; // 讓 dtor 成為 virtual
Base(Base&&) = default; // 支援 move
Base& operator=(Base&&) = default;
Base(const Base&) = default; // 支援 copy
Base& operator=(const Base&) = default;
};
即使編譯器願意生成且行為正確,主動宣告 + = default 仍是好政策:意圖更清楚,還能避開隱蔽 bug——
class StringTable {
public:
StringTable()
{ makeLogEntry("Creating StringTable object"); } // 後來加入 logging
~StringTable() // 也加了 dtor —— 問題就在這
{ makeLogEntry("Destroying StringTable object"); }
private:
std::map<int, std::string> values;
};
加入 destructor 後,move 操作不再生成,但 copy 照舊——所有「move」
StringTable 的程式碼照樣編譯、照樣通過測試,實際卻在複製底層的 std::map<int, std::string>,可能比 move 慢幾個數量級。若當初以 = default 明確宣告 copy 與 move,就不會發生。成員函式模板不抑制 SMF 生成
class Widget {
template<typename T> // 可從任何型別建構 Widget
Widget(const T& rhs);
template<typename T> // 可從任何型別賦值
Widget& operator=(const T& rhs);
};
即使 T = Widget 時模板可實例化出與 copy ctor / copy assign 相同的簽章,成員函式模板永遠不算「使用者宣告的 copy/move 操作」,編譯器照常生成 copy 與 move 操作(在一般條件滿足時)。看似邊角案例,但 Item 26 證明它有重要後果(生成的 copy ctor 與模板會同時參與 overload resolution)。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
| 類別只宣告了 destructor,move 還會生成嗎? | 不會;move 請求退回 copy(copy 仍生成,但已 deprecated) |
| 宣告 move ctor 後,copy 操作的狀態? | 被 delete(不是「不生成」) |
| 宣告 copy ctor 後,copy assign 還會生成嗎? | 會(兩個 copy 操作彼此獨立),但此生成已 deprecated |
| 宣告 move ctor 後,move assign 還會生成嗎? | 不會(兩個 move 操作彼此不獨立) |
| move 生成的三條件 | 無使用者宣告的 copy 操作、move 操作、destructor |
| 「加了 logging destructor 之後程式變慢」 | move 被抑制 → 隱性退回 copy;解法:= default 明確宣告 copy/move |
| 多型基底類別的標準寫法 | virtual ~Base() = default; + 四個 copy/move 操作 = default |
成員模板 Widget(const T&) 會抑制 SMF 生成嗎? |
永遠不會(即使 T=Widget 可產生相同簽章;Item 26 有後續) |
| 生成的 destructor 的例外規格 | 預設 noexcept(見 Item 14) |
| default constructor 的生成條件 | 類別沒有任何使用者宣告的建構子(與 C++98 相同) |
| 「memberwise move」的真義 | 對各成員套用 std::move 的請求;不可 move 的成員改用 copy |
Related Notes
- 04-Moving-To-Modern-Cpp/05-Deleted-Functions — 宣告 move 操作時,copy 操作正是以
= delete的機制被停用 - 04-Moving-To-Modern-Cpp/08-Noexcept — 編譯器生成的 destructor 與 SMF 預設為 noexcept
- 06-Move-Semantics-And-Perfect-Forwarding/07-Move-Operation-Realities — 假設 move 不存在、不便宜、沒被用到:本 Item 的生成規則是主因之一
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward — memberwise move 的核心是對成員套用
std::move後的 overload resolution - 06-Move-Semantics-And-Perfect-Forwarding/04-Overloading-On-Universal-References — 成員模板不抑制 SMF 生成所引發的 overload resolution 陷阱
- 05-Smart-Pointers/05-Pimpl-Idiom — Pimpl 類別中 SMF 必須在實作檔定義的實例