Item 30:熟悉完美轉發的失敗案例 (Familiarize Yourself with Perfect Forwarding Failure Cases)

Overview Table

失敗案例 失敗原因 症狀 解法
Braced initializers {1,2,3} 對非 std::initializer_list 的模板參數屬 non-deduced context,禁止推導 編譯錯誤 先用 auto il = {1,2,3}; 建立區域變數再轉發
0 / NULL 當空指標 推導成整數型別(通常 int)而非指標 行為錯誤(轉發的是整數) 一律改用 nullptr
僅宣告的整數 static const 成員 universal reference 形同取址,缺定義即無位址可指 編譯過、連結失敗 .cpp 補上定義(不重複初始值)
多載函式名 / 模板名 名稱本身無型別 → 無法推導要哪個多載/實體化 編譯錯誤 用函式指標型別明確指定:先存入具型別的指標變數,或 static_cast<FuncPtrType>(name)
Bitfields(非 const) 非 const reference 不得繫結 bitfield(無法定址任意 bits) 編譯錯誤 static_cast 複製到一般整數變數再轉發複本
失敗的統一定義

f(expr) 做一件事,而 fwd(expr) 做另一件事,完美轉發即告失敗。根本原因只有兩種:模板型別推導失敗(不能編譯),或推導出「錯誤」的型別(實體化失敗,或呼叫到與直接呼叫不同的多載)。

完美轉發的機制與失敗的兩種模式

完美轉發(perfect forwarding):轉發函式把參數連同其型別、lvalue/rvalue 屬性、const/volatile 屬性原封不動傳給目標函式。這排除了 by-value(是複本)與 pointer(強迫呼叫端傳指標),因此必然使用 universal reference——只有它能編碼引數的 lvalueness/rvalueness。

template<typename T>
void fwd(T&& param)                // 接受任何引數
{
  fforward<T>(param);       // 轉發給 f
}

template<typename... Ts>
void fwd(Ts&&... params)           // variadic 版本:接受任意數量引數
{
  fforward<Ts>(params)...;  // 標準容器的 emplace 系列(Item 42)與
}                                  // make_shared / make_unique(Item 21)都是這種形式

直接呼叫與經由 fwd 呼叫,編譯器做的事完全不同:

直接呼叫:  f( expr ) ──► 比對「呼叫端引數」vs「f 的參數宣告」
                          └─ 必要時做隱式轉換(如 {1,2,3} → vector<int>)→ 成功

間接呼叫:  fwd( expr ) ──► 先對 expr 做模板型別推導,再以推導結果比對 f 的參數
                            │
                            ├─ 推導失敗(braced initializer、無型別的名稱)→ 編譯錯誤
                            └─ 推導出「錯誤」型別(如 0 → int)
                               └─ 實體化失敗,或呼叫到不同的 f 多載 → 行為分歧

編譯期推導失敗:braced initializers、0/NULL、函式與模板名稱

Braced initializers — 傳給「未宣告為 std::initializer_list」的模板參數是標準所稱的 non-deduced context,編譯器被禁止推導:

void f(const std::vector<int>& v);

f({ 1, 2, 3 });     // OK:{1,2,3} 隱式轉換成暫時的 std::vector<int>
fwd({ 1, 2, 3 });   // 錯誤!non-deduced context,禁止推導

// 解法:Item 2 — auto 會把 braced initializer 推導成 std::initializer_list
auto il = { 1, 2, 3 };   // il 的型別是 std::initializer_list<int>
fwd(il);                 // OK:il 有明確型別,完美轉發給 f

0 / NULL — 模板推導成整數型別(通常 int)而非空指標,轉發後意義全變。解法:永遠傳 nullptr(見 Item 8)。

多載函式名與模板名 — 名稱本身沒有單一型別,而 fwd 是模板、無參數型別資訊可供篩選,推導無從進行:

void f(int (*pf)(int));            // 也可寫成 void f(int pf(int)),意義相同
int processVal(int value);
int processVal(int value, int priority);   // processVal 是多載名稱

f(processVal);      // OK:f 的參數型別讓編譯器選出單參數版本
fwd(processVal);    // 錯誤!哪個 processVal?無型別 → 無法推導

template<typename T>
T workOnVal(T param);   // 模板名代表「一族函式」
fwd(workOnVal);         // 錯誤!哪個 instantiation?

// 解法:手動指定需要的多載 / 實體化
using ProcessFuncType = int (*)(int);            // alias declaration(Item 9)
ProcessFuncType processValPtr = processVal;      // 指定所需簽章 → 選出正確版本
fwd(processValPtr);                              // OK
fwd(static_cast<ProcessFuncType>(workOnVal));    // OK:明確指定實體化
前提:你得知道 fwd 最終轉發到的函式期望什麼函式指標型別——完美轉發函式的設計就是「什麼都收」,因此其文件應告訴你該傳什麼。

連結期與綁定限制:static const 成員、bitfields

僅宣告的整數 static const 資料成員 — 編譯器對這類成員做 const propagation(把值 28 直接塞進所有使用處),通常不需配置儲存空間,因此宣告即可用:

class Widget {
public:
  static const std::size_t MinVals = 28;  // 只有宣告(無定義)
};
void fsize_t val;

fMinVals);     // OK:視同 f(28
fwdMinVals;   // 能編譯,但(依標準)不應能連結!

原因:fwd 的參數是 universal reference,而 reference 在產生的機器碼中通常就是指標——傳 reference 形同取址,必須有真實記憶體可指。可移植的解法是在實作檔提供定義:

const std::size_t Widget::MinVals;   // 放在 Widget 的 .cpp;不重複初始值 28
「不應能連結」的例外

標準要求以 reference 傳遞時該成員必須有定義,但並非所有實作都強制執行——某些編譯器/連結器下未定義也能連結成功。這種程式碼不可移植,不要依賴。(補充:C++17 起 static constexpr / inline 變數隱含類外定義,此問題已緩解,惟超出本書 C++11/14 範圍。)

Bitfields — 標準明文規定「a non-const reference shall not be bound to a bit-field」。bitfield 可能佔機器字組中的任意幾個 bits(如 32-bit int 的第 3-5 bits),而 C++ 可定址的最小單位是 char——沒有指向任意 bits 的指標,也就沒有繫結任意 bits 的 reference:

struct IPv4Header {
  std::uint32_t version:4, IHL:4, DSCP:6, ECN:2,
                totalLength:16;    // bitfields
};
void fsize_t sz;

IPv4Header h;
f(h.totalLength);     // OK:by-value 拿到 bitfield 值的複本
fwd(h.totalLength);   // 錯誤!非 const reference 不能繫結 bitfield

// 解法:自己先複製一份再轉發(初始化形式見 Item 6)
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length);          // OK:轉發複本

任何函式收到 bitfield 引數時,拿到的必然是值的複本(不存在指向 bitfield 的指標),所以「先複製再轉發」不會遺失任何資訊。

「reference 不能繫結 bitfield」僅限非 const

reference-to-const 可以接受 bitfield 引數——但標準要求它實際繫結的是「存有 bitfield 值複本的標準整數型別物件」(如 int),繫結的仍是複本而非 bitfield 本身。

Exam/Test Patterns

情境關鍵字 答案
完美轉發失敗的兩種根本模式 (1) 型別推導失敗(編譯錯誤);(2) 推導出錯誤型別(實體化失敗或呼叫到不同多載)
五大失敗案例(背誦題) braced initializers、0/NULL 空指標、僅宣告的整數 static const 成員、模板名與多載函式名、bitfields
f({1,2,3}) 成功但 fwd({1,2,3}) 編譯失敗 braced initializer 是 non-deduced context;先 auto il = {1,2,3};fwd(il)
直接呼叫與轉發呼叫為何不對稱 直接呼叫時編譯器看得到 f 的參數型別可做隱式轉換;經 fwd 必須先對引數推導型別
fwd(0)f 收到什麼 整數 0(型別 int),不是空指標;改傳 nullptr
fwdMinVals 編譯過但連結失敗 僅宣告未定義的 static const 成員;reference 形同取址;在 .cppconst std::size_t Widget::MinVals;(不重複初始值)
fwd(overloadedName) / fwd(templateName) 錯誤 名稱無型別無法推導;先 using FP = int(*)(int); 建具型別指標,或 static_cast<FP>(name)
fwd(h.totalLength)(bitfield)錯誤 非 const reference 不得繫結 bitfield;static_cast 複製到區域變數再轉發複本
emplace / make_shared 為何吃不下 {...} 它們就是 variadic 完美轉發函式,繼承本 Item 全部失敗案例