Item 23:理解 std::move 與 std::forward (Understand std::move and std::forward)

Overview Table

面向 std::move std::forward
本質 函式模板,執行 cast(轉型) 函式模板,執行 cast(轉型)
轉型條件 無條件轉成 rvalue 有條件:僅當引數綁定 rvalue 時才轉成 rvalue
實際搬移資料? 否——它不搬移任何東西 否——它不轉發任何東西
執行期成本 零,不產生任何可執行碼 零,不產生任何可執行碼
需要模板型別引數? 不需要,std::move(x) 需要,std::forward<T>(x)T非參考型別)
典型使用場景 rvalue reference 參數(如 move ctor 內) universal reference 參數(轉發給其他函式)
語意 「此物件可以被搬移」的標記 保留引數原本的 lvalue/rvalue 屬性再傳遞
Important

「Things to Remember」三要點:

  1. std::move 對引數做無條件 rvalue 轉型,它本身不搬移任何東西
  2. std::forward 只有在引數綁定到 rvalue 時,才把引數轉型為 rvalue。
  3. std::movestd::forward執行期都不做任何事(純編譯期轉型)。

std::move:無條件的 rvalue cast

std::move 只是把引數 static_cast 成 rvalue reference,好讓編譯器知道「此物件有資格被搬移」;名字若叫 rvalue_cast 反而更貼切。

// C++11 近似實作(namespace std)
template<typename T>
typename remove_reference<T>::type&&   // 回傳型別:去除參考後加 &&
move(T&& param)                        // param 是 universal reference(見 Item 24)
{
  using ReturnType =
    typename remove_reference<T>::type&&;   // alias declaration(見 Item 9)
  return static_cast<ReturnType>(param);    // 唯一的工作:轉型
}

// C++14 版:回傳型別推導 + remove_reference_t,更簡潔
template<typename T>
decltype(auto) move(T&& param)
{
  using ReturnType = remove_reference_t<T>&&;
  return static_cast<ReturnType>(param);
}
關鍵細節 說明
為何要 remove_reference T 被推導為 lvalue reference,T&& 經 reference collapsing(見 Item 28)會變回 lvalue reference;先去除參考再加 &&保證回傳 rvalue reference
為何回傳值是 rvalue? 函式回傳的 rvalue reference 本身是 rvalue
唯一保證 對物件套用 std::move 後,你唯一確定的是:結果是一個 rvalue

const 陷阱:move 請求會靜默退化成 copy

rvalue 只是「通常」是搬移的候選者。對 const 物件呼叫 std::move,得到的是 const rvalue——它無法綁定到 move ctor 的 string&&(non-const),卻能綁定到 copy ctor 的 const string&(lvalue-reference-to-const 允許綁定 const rvalue),於是靜默地呼叫 copy

class Annotation {
public:
  explicit Annotation(const std::string text)  // 依 Item 41 傳值,但誤加了 const
  : valuemove(text)   // 看似搬移,實際上是「複製」!
  { /* ... */ }              // std::move(text) 是 const std::string 型別的 rvalue
private:
  std::string value;
};

class string {               // std::string 的兩個候選建構子
public:
  string(const string& rhs); // copy ctor:可綁定 const rvalue → 被選中
  string(string&& rhs);      // move ctor:只收 non-const rvalue → 不可行
};
例外與教訓

  • 想被搬移的物件不要宣告成 const:對 const 物件的 move 請求會被靜默轉為 copy,程式照常編譯、執行,只是效能不如預期,極難察覺。
  • std::move 不保證轉型後的物件真的有資格被搬移(除了 const 之外,型別也可能根本沒有 move 操作——見 Item 29:06-Move-Semantics-And-Perfect-Forwarding/07-Move-Operation-Realities)。
  • 這種「const → copy」行為是刻意設計:搬移通常會修改來源物件,語言不允許 const 物件被傳給可能修改它的函式,以維持 const-correctness

std::forward:有條件的 rvalue cast

函式參數永遠是 lvalue(即使其型別是 rvalue reference)。轉發時若不處理,永遠只會命中 lvalue 多載。std::forward<T> 依編碼在模板參數 T 中的資訊(機制見 Item 28),只在原始引數是 rvalue 時才轉型為 rvalue。

void process(const Widget& lvalArg);   // 處理 lvalue
void process(Widget&& rvalArg);        // 處理 rvalue

template<typename T>
void logAndProcess(T&& param)          // param 是 universal reference
{
  auto now = std::chrono::system_clock::now();
  makeLogEntry("Calling 'process'", now);
  processforward<T>(param);     // 保留引數原本的 lvalue/rvalue 屬性
}

Widget w;
logAndProcess(w);              // 傳入 lvalue → process(const Widget&)
logAndProcessmove(w));   // 傳入 rvalue → process(Widget&&
呼叫端引數            logAndProcess 內部               std::forward<T>(param)
─────────────        ──────────────────────           ─────────────────────
w        (lvalue) →  T = Widget&   param 是 lvalue →  不轉型 → 仍是 lvalue ─→ process(const Widget&)
move(w)  (rvalue) →  T = Widget    param 是 lvalue →  轉型   → 變成 rvalue ─→ process(Widget&&)
                     ↑ 引數的 value category 編碼在 T 中(Item 28)

為何不用 std::forward 取代 std::move?

技術上 std::forward 可以模擬 std::move 的一切行為,但慣例上兩者分工明確:

class Widget {
public:
  Widget(Widget&& rhs)                       // 慣用寫法
  : smove(rhs.s)                      // 簡潔、不易出錯
  { ++moveCtorCalls; }

  // Widget(Widget&& rhs)                    // 反面教材:可行但不該用
  // : sstring>(rhs.s)   // 必須手寫非參考型別引數
  // { ++moveCtorCalls; }                    // 寫錯成 std::string& 就變 copy
private:
  static std::size_t moveCtorCalls;
  std::string s;
};
比較點 std::move std::forward
打字量 少(只要函式引數) 多(還要模板型別引數)
出錯機率 高(誤傳 std::string& → 悄悄變 copy construct)
傳達的語意 無條件轉 rvalue,準備搬移 有條件轉 rvalue,保留原始屬性地轉發

兩者代表截然不同的動作,用兩個不同名字的函式區分是好設計。

Exam/Test Patterns

情境關鍵字 答案
std::move 在執行期做什麼? 什麼都不做:純編譯期轉型,不產生任何可執行碼
std::move 的本質 無條件將引數 static_cast 為 rvalue(更貼切的名字是 rvalue_cast
std::forward 何時轉型? 只有當引數當初以 rvalue 初始化時才轉為 rvalue(conditional cast)
const std::string 呼叫 std::move 再建構成員 呼叫 copy ctor(const rvalue 無法綁定 string&&,但可綁定 const string&
想讓物件可被搬移 不要宣告成 const
std::move 保證物件會被搬移嗎? 否,唯一保證是結果為 rvalue;能否搬移取決於型別(const、有無 move 操作)
std::forward 如何得知原始引數是 lvalue 或 rvalue? 資訊編碼在模板參數 T 中(reference collapsing,見 Item 28)
move 實作中為何用 remove_reference 防止 T 為 lvalue reference 時 T&& 折疊回 lvalue reference,確保回傳 rvalue reference
型別為 Widget&& 的函式參數本身是? lvalue(所有具名參數皆為 lvalue,與其型別無關)
std::forward 的型別引數慣例 非參考型別表示引數為 rvalue;誤傳 T& 會退化為 copy
rvalue reference 參數 vs universal reference 參數該用哪個? rvalue reference → std::move;universal reference → std::forward(見 Item 25)