Item 29:假設 move 操作不存在、不便宜、也不會被使用 (Assume that move operations are not present, not cheap, and not used)

Overview Table

重點 說明
核心心態 不了解型別的情況下(尤其泛型程式碼),預設 move 操作「不存在、不便宜、不會被使用
不存在 (not present) 未針對 C++11 改寫的型別沒有 move 操作;宣告了 copy 操作、move 操作或解構子的 class 也不會自動生成 move(Item 17)
不便宜 (not cheap) std::array 的 move 是線性時間;SSO 之下的短 std::string move 不比 copy 快
不會被使用 (not used) 需要 strong exception safety guarantee 的情境下,move 未宣告 noexcept 就會退回 copy(Item 14)
來源是 lvalue 幾乎只有 rvalue 能當 move 的來源;lvalue 來源通常導致 copy
例外 型別已知且確定支援廉價 move,就不需要悲觀假設,可放心依賴 move semantics

move 失效的四種情境

move semantics 雖是 C++11 招牌特性,但「重新編譯就變快」是被誇大的傳說:只有已為 C++11 改版的型別(如標準函式庫容器)才享受得到。以下情境中 move 不會帶來效能提升:

情境 原因 結果
No move operations 型別未提供 move 操作(舊 C++98 程式碼、或宣告了 copy/解構子而抑制自動生成) move 請求默默變成 copy
Move not faster move 操作不比 copy 快(如 std::array、SSO 短字串) 語法上是 move,效能上無差別
Move not usable 上下文要求不拋例外的 move,但 move 未宣告 noexcept 編譯器被迫改用 copy
Source object is lvalue move 的來源幾乎必須是 rvalue lvalue 來源不觸發 move
class Widget98 {          // 舊型別:只有 copy 操作
public:
  Widget98(const Widget98&);            // 宣告了 copy 建構子
  Widget98& operator=(const Widget98&); // 與 copy 賦值
  // → 編譯器「不會」自動生成 move 操作(Item 17)
};

Widget98 w1;
auto w2 = std::move(w1);  // 語法合法,但實際呼叫 copy 建構子!
                          // rvalue 可以綁定到 const Widget98&

成員或基底類別停用 move(例如以 =delete 刪除 move 操作,見 Item 11)同樣會抑制編譯器生成 move。

「lvalue 不能作為 move 來源」有少數例外

例如 Item 25 提到的情形:函式以值回傳綁定到 rvalue reference 或 universal reference 的參數時,應對其套用 std::movestd::forward;此外編譯器對符合條件的回傳值有 RVO/隱式 move 等特殊處理。原書用語是「with very few exceptions」,並非絕對。

std::move(w1) 不保證發生移動

std::move 只做 cast,不移動任何東西。若型別沒有 move 操作,結果就是呼叫 copy——編譯器不會警告你

不便宜之一:std::array 的線性時間 move

多數標準容器(如 std::vector)把內容放在 heap,物件本身概念上只持有一根指標,所以 move 是常數時間——只要搬指標。std::array 則把資料直接存放在物件內,沒有指標可搬,move 必須逐一移動每個元素,是線性時間

std::vector<Widget> 的 move(常數時間 O(1)):
  vw1 [ptr] ──┐                move 後: vw1 [null]
              ▼                          vw2 [ptr] ──► Widgets (heap)
        Widgets (heap)          只改兩根指標,元素完全不動

std::array<Widget, 10000> 的 move(線性時間 O(n)):
  aw1 [W W W ... W]  ──每個元素逐一 move──►  aw2 [W W W ... W]
      (moved-from)                              (moved-to)
std::vector<Widget> vw1;
// ... 填入資料
auto vw2 = std::move(vw1);   // O(1):只複製指標並將來源設為 null

std::array<Widget, 10000> aw1;
// ... 填入資料
auto aw2 = std::move(aw1);   // O(n):10000 個元素逐一被 move
std::array 仍然「支援」move

Widget 的 move 比 copy 快,move 整個 std::array<Widget, N> 仍比 copy 快——但兩者同為線性複雜度,遠非「搬容器像搬指標一樣便宜」的傳說。

不便宜之二:std::string 與 SSO

std::string 提供常數時間 move、線性時間 copy,聽起來 move 必勝——但許多實作採用 SSO(small string optimization,小字串最佳化)

字串長度 儲存位置 move vs copy
短字串(如 ≤15 字元) std::string 物件內部緩衝區 move 不比 copy 快(沒有指標可搬,都得搬字元)
長字串 heap move 為常數時間,明顯快於 copy
std::string s1 = "short";          // SSO:內容存在物件內部
auto s2 = std::move(s1);           // 實際仍需搬移字元,與 copy 同級

std::string s3(100000, 'x');       // 長字串:內容在 heap
auto s4 = std::move(s3);           // 真正的 O(1) 指標搬移

SSO 的動機是統計上短字串是常態,省下動態配置整體是贏——代價是這些字串的 move 不再有優勢(樂觀的說法:copy 也不比 move 慢)。

不會被使用:noexcept 與 strong guarantee

Item 14 提過:部分標準函式庫容器操作(如 std::vector::push_back 擴容)提供 strong exception safety guarantee。為了不破壞依賴此保證的 C++98 舊程式碼,C++11 規定:只有當 move 操作已知不拋例外時,底層才能以 move 取代 copy

class Widget {
public:
  Widget(Widget&& rhs);            // 有 move,但「沒有」宣告 noexcept
};

std::vector<Widget> vw;
// vw 擴容重新配置時:
// 即使 Widget 有更快的 move、且元素搬遷是絕佳的 move 時機,
// 編譯器仍被迫呼叫 copy 建構子(因為 move 可能拋例外,
// 一旦中途拋出就無法回復,會破壞 strong guarantee)

因此:move 存在 ≠ move 會被呼叫。想讓標準容器真正用上你的 move,請宣告 noexcept(見 Item 14 的條件式 noexcept 討論)。

何時可以不必悲觀假設

程式碼型態 態度
泛型程式碼(template,型別未知) 悲觀假設:像 C++98 一樣保守看待複製成本
不穩定程式碼(所用型別特性頻繁變動) 同上,悲觀假設
型別已知且特性穩定 不需要假設:直接查閱該型別的 move 支援;若 move 廉價且使用情境會觸發 move,可放心依賴 move 取代 copy

這正是本 Item「Things to Remember」的兩句話:預設悲觀(not present, not cheap, not used),但已知型別可查證後放心使用。

Exam/Test Patterns

情境關鍵字 答案 「舊 C++98 型別 + std::move,發生什麼?」 沒有 move 操作 → rvalue 綁到 const T&呼叫 copy,語法合法、無警告 「宣告了解構子/copy 操作的 class,會自動生成 move 嗎?」 不會(Item 17);成員或基底類別刪除 move 也會抑制生成 「move std::vector vs move std::array 的複雜度?」 std::vector Oarray O(n(資料在物件內,逐元素 move) 「短的 std::string move 比 copy 快嗎?」 不一定:SSO 把短字串存在物件內部緩衝區,move 無指標可搬,不比 copy 快 「有 move 操作但 push_back 擴容仍呼叫 copy?」 move 未宣告 noexcept,strong exception guarantee 迫使容器用 copy 「寫 template 時該如何看待 move?」 型別未知 → 假設 move 不存在、不便宜、不會被用,像 C++98 一樣保守 「型別已知且穩定時?」 不必假設,查閱型別的 move 支援後可放心依賴 move semantics 「move 效能提升失效的四種情境?」 no move operations / move not faster / move not usable (noexcept) / source is lvalue