Item 25:對 rvalue reference 用 std::move、對 universal reference 用 std::forward (Use std::move on rvalue references, std::forward on universal references)
Overview Table
| 情境 | 正確做法 | 原因 |
|---|---|---|
參數是 rvalue reference(Widget&& rhs) |
最後一次使用時套 std::move |
它一定綁定到可移動的物件 → 無條件轉成 rvalue |
參數是 universal reference(T&& param) |
最後一次使用時套 std::forward<T> |
它可能綁定到 lvalue 或 rvalue → 有條件轉型 |
對 universal reference 用 std::move |
禁止 | 會意外把呼叫端的 lvalue(如區域變數)搬空 |
對 rvalue reference 用 std::forward |
避免 | 雖可運作,但冗長、易錯、不合慣例 |
| return by value 時回傳 rvalue/universal reference 參數 | 在 return 中套 std::move / std::forward |
讓回傳值走 move 而非 copy |
| return by value 時回傳區域變數 | 不要套 std::move / std::forward |
會破壞 RVO(return value optimization) |
核心規則:無條件 move vs. 有條件 forward
Rvalue reference 只會綁到可移動的物件,因此轉發給其他函式時應無條件轉成 rvalue(std::move);universal reference 只有在被 rvalue 初始化時才該轉成 rvalue,因此要用有條件轉型的 std::forward。
class Widget {
public:
Widget(Widget&& rhs) // rhs 是 rvalue reference:
: namemove(rhs.name), // 一定綁到可移動物件,
pmove(rhs.p) {} // 無條件用 std::move
template<typename T>
void setName(T&& newName) // newName 是 universal reference:
{ name = std::forward<T>(newName); } // 可能是 lvalue,用 std::forward
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
錯用 std::move 於 universal reference 的災難:
template<typename T>
void setName(T&& newName)
{ name = std::move(newName); } // 可編譯,但大錯特錯!
std::string getWidgetName(); // 工廠函式
Widget w;
auto n = getWidgetName(); // n 是區域變數(lvalue)
w.setName(n); // n 的值被搬進 w!
// 之後 n 的值不確定(unspecified)
呼叫端以為 setName(n) 是唯讀操作,實際上 n 被搬空——這種行為會讓使用者崩潰。
為何不用「lvalue/rvalue 多載」取代 universal reference
把 setName 改寫成 const std::string& 與 std::string&& 兩個多載雖可避免上述問題,但有三大缺點:
| 缺點 | 說明 |
|---|---|
| 程式碼變多 | 一個模板變成兩個函式,維護成本加倍 |
| 效率可能較差 | w.setName("Adela Novak"):universal reference 版直接把字面值指派給 name;多載版則要建立暫時 std::string → move → 解構(多一次建構與解構) |
| 擴展性極差(最嚴重) | n 個參數需要 2^n 個多載;std::make_shared / std::make_unique 這類參數數量不定的函式只能用 universal reference + std::forward |
template<class T, class... Args> // C++11 標準庫
shared_ptr<T> make_shared(Args&&... args); // 內部對 args
// 套 std::forward
template<class T, class... Args> // C++14 標準庫
unique_ptr<T> make_unique(Args&&... args);
只在「最後一次使用」時轉型
若同一函式內要多次使用該參考綁定的物件,必須確保它在用完之前不被搬走——只對最後一次使用套 std::move / std::forward:
template<typename T>
void setSignText(T&& text) // text 是 universal reference
{
sign.setText(text); // 使用 text,但不修改它
auto now = std::chrono::system_clock::now();
signHistory.add(now,
std::forward<T>(text)); // 最後一次使用才有條件轉成 rvalue
}
std::move_if_noexcept
對 rvalue reference 套用「最後一次使用才 move」的原則時,少數情況應改用 std::move_if_noexcept——當 move 建構子未宣告 noexcept 時退回 copy,以維持強例外安全保證。詳見 04-Moving-To-Modern-Cpp/08-Noexcept(Item 14)。
Return by value 時同理:回傳綁定到 rvalue/universal reference 的物件時,在 return 中套 std::move / std::forward,可把 copy 變成 move;即使型別不支援 move 也無害(rvalue 會被 copy 建構子接走)。
Matrix operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // 把 lhs 移進回傳值(省一次 copy)
}
template<typename T>
Fraction reduceAndCopy(T&& frac) // by-value 回傳
{
frac.reduce();
return std::forward<T>(frac); // rvalue 就 move,lvalue 就 copy
}
陷阱:對區域變數 return std::move 反而更慢(RVO)
Widget makeWidget() {
Widget w;
return w; // 正確:RVO 直接在回傳值位置建構 w,零 copy 零 move
}
Widget makeWidget() {
Widget w;
return std::move(w); // 錯誤示範!回傳的是「參考」而非區域物件本身
} // → 不符合 RVO 條件,強制執行一次 move
RVO 成立的兩個條件:(1) 區域物件型別與函式回傳型別相同;(2) return 的就是該區域物件本身。std::move(w) 回傳的是參考,違反條件 (2),反而排除了 RVO 的可能。
return 敘述決策流程(by-value 函式)
────────────────────────────────────────────
回傳的是什麼?
├─ rvalue ref / universal ref 參數
│ → 套 std::move / std::forward (copy → move)
├─ 區域變數(符合 RVO 條件)
│ → 直接 return w (RVO:零成本)
│ └─ 編譯器若不做 RVO
│ → 標準規定「隱含視為 rvalue」自動 move
└─ by-value 參數
→ 直接 return w(不適用 RVO,但編譯器必須隱含 move)
標準規定:只要 RVO 條件成立,編譯器要嘛執行 copy elision,要嘛必須把回傳物件隱含視為 rvalue(等同自動套 std::move)。因此手寫 return std::move(w) 幫不了編譯器,只會阻礙最佳化。此規則對 std::forward 同樣成立:絕不對符合 RVO 資格的區域物件套 std::move 或 std::forward。
函式的 by-value 參數不符合 RVO 資格(copy elision 不適用),但回傳時編譯器仍必須將其視為 rvalue(隱含 move)——所以同樣不要手寫 std::move。另外,對區域變數套 std::move 並非永遠錯誤:把它傳給其他函式且之後不再使用時是合理的;錯的是用在符合 RVO 資格的 return 敘述中。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
參數型別 Widget&&,要轉發給其他函式 |
std::move(無條件轉 rvalue) |
參數型別 T&&(模板 + 型別推導),要轉發 |
std::forward<T>(有條件轉 rvalue) |
universal reference 內用了 std::move,呼叫端傳入區域變數 |
區域變數被搬空,值變 unspecified——經典錯誤 |
為什麼不寫 const T& + T&& 兩個多載就好 |
程式碼加倍、可能多一次暫時物件建構、n 參數要 2^n 多載、變參模板無法多載 |
| 同一參考在函式內用多次 | 只對最後一次使用套 move/forward |
| by-value 回傳 rvalue/universal reference 參數 | return std::move(lhs); / return std::forward<T>(frac); |
by-value 回傳區域變數,return std::move(w) 好嗎 |
不好!破壞 RVO,強制多一次 move |
| RVO 兩條件 | 區域物件型別 = 回傳型別;return 的是該區域物件本身 |
| 編譯器沒做 RVO 會發生什麼 | 標準要求隱含把回傳的區域物件/by-value 參數視為 rvalue(自動 move) |
何時用 std::move_if_noexcept |
move 建構子可能拋例外、需要強例外安全保證時(Item 14) |
Related Notes
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward — Item 23:
std::move/std::forward本質都是轉型(cast),本篇規則的基礎 - 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References — Item 24:如何分辨 universal reference 與 rvalue reference(本篇規則的前提)
- 06-Move-Semantics-And-Perfect-Forwarding/04-Overloading-On-Universal-References — Item 26:本篇建議「用 universal reference 取代多載」,但 universal reference 本身不該拿來多載
- 06-Move-Semantics-And-Perfect-Forwarding/07-Move-Operation-Realities — Item 29:不是所有型別的 move 都便宜,多載 vs. universal reference 的效能差距因此放大
- 04-Moving-To-Modern-Cpp/08-Noexcept — Item 14:
noexcept與std::move_if_noexcept的關聯 - 05-Smart-Pointers/04-Make-Unique-And-Make-Shared — Item 21:
make_shared/make_unique是「只能用 universal reference + forward」的代表案例