Select a result to preview
| 重點 | 說明 |
|---|---|
| 核心心態 | 在不了解型別的情況下(尤其泛型程式碼),預設 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 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。
例如 Item 25 提到的情形:函式以值回傳綁定到 rvalue reference 或 universal reference 的參數時,應對其套用 std::move/std::forward;此外編譯器對符合條件的回傳值有 RVO/隱式 move 等特殊處理。原書用語是「with very few exceptions」,並非絕對。
std::move(w1) 不保證發生移動
std::move 只做 cast,不移動任何東西。若型別沒有 move 操作,結果就是呼叫 copy——編譯器不會警告你。
多數標準容器(如 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 提供常數時間 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 慢)。
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),但已知型別可查證後放心使用。
std::move,發生什麼?」const T& → 呼叫 copy,語法合法、無警告std::vector vs move std::array 的複雜度?」std::vector Oarray O(n(資料在物件內,逐元素 move)
std::string move 比 copy 快嗎?」push_back 擴容仍呼叫 copy?」noexcept,strong exception guarantee 迫使容器用 copynoexcept) / source is lvaluestd::move 只是 cast,不保證移動;本 Item 說明「不保證」的具體場景noexcept 時,strong guarantee 情境退回 copy 的根源