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 一律是 publicinline、非 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
};
「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
例外:基於 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