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」三要點:
std::move對引數做無條件 rvalue 轉型,它本身不搬移任何東西。std::forward只有在引數綁定到 rvalue 時,才把引數轉型為 rvalue。std::move與std::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) |
Related Notes
- 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References —
std::move的參數T&&即 universal reference;區分 rvalue reference 與 universal reference 是選用 move/forward 的前提 - 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage — 何時該用
std::move、何時該用std::forward的實務準則(最後一次使用時套用、回傳值情境) - 06-Move-Semantics-And-Perfect-Forwarding/06-Reference-Collapsing —
std::forward之所以能「還原」引數的 value category,底層機制即 reference collapsing - 06-Move-Semantics-And-Perfect-Forwarding/07-Move-Operation-Realities — move 不一定存在、不一定便宜:
std::move之後也未必真的搬移 - 01-Introduction/01-Terms-And-Conventions — lvalue/rvalue 的基本定義;「參數永遠是 lvalue」的出處
- 04-Moving-To-Modern-Cpp/03-Alias-Declarations —
std::move實作中使用的remove_reference_talias template(Item 9) - 09-Tweaks/01-Pass-By-Value — Annotation 範例中「傳值參數 + std::move 進成員」的模式出自 Item 41