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-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 形同取址;在 .cpp 補 const 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 全部失敗案例 |
Related Notes
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward —
std::forward的原理,轉發函式保留 lvalue/rvalue 屬性的核心工具 - 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References — 完美轉發必用 universal reference 的原因
- 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage — 轉發函式內對 universal reference 使用
std::forward的守則 - 06-Move-Semantics-And-Perfect-Forwarding/06-Reference-Collapsing — 完美轉發背後的推導與 reference collapsing 機制
- 02-Deducing-Types/02-Auto-Type-Deduction —
auto對 braced initializer 推導成std::initializer_list的特例(workaround 依據) - 04-Moving-To-Modern-Cpp/02-Nullptr —
0/NULL在模板推導中變成整數的完整說明 - 04-Moving-To-Modern-Cpp/03-Alias-Declarations — 以 alias declaration 定義函式指標型別
- 03-Auto/02-Explicitly-Typed-Initializer-Idiom — bitfield workaround 使用的
static_cast初始化形式 - 05-Smart-Pointers/04-Make-Unique-And-Make-Shared — make 函式即完美轉發函式,
{}無法轉發 - 09-Tweaks/02-Emplacement — emplace 系列同為 variadic 完美轉發,共享這些失敗案例