Item 25:對 rvalue reference 用 std::move、對 universal reference 用 std::forward (Use std::move on rvalue references, std::forward on universal references)

Overview Table

情境 正確做法 原因
參數是 rvalue referenceWidget&& rhs 最後一次使用時套 std::move 一定綁定到可移動的物件 → 無條件轉成 rvalue
參數是 universal referenceT&& 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)
不必為「保險」加 std::move

標準規定:只要 RVO 條件成立,編譯器要嘛執行 copy elision,要嘛必須把回傳物件隱含視為 rvalue(等同自動套 std::move)。因此手寫 return std::move(w) 幫不了編譯器,只會阻礙最佳化。此規則對 std::forward 同樣成立:絕不對符合 RVO 資格的區域物件套 std::movestd::forward

例外:by-value 參數

函式的 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)