Item 1:理解模板型別推導 (Understand Template Type Deduction)
Overview Table
模板呼叫 f(expr) 時,編譯器由 expr 同時推導 T 與 ParamType(param 的完整型別)。推導結果取決於 ParamType 的形式,共分三種 Case:
| Case | ParamType 形式 | 推導規則 | 關鍵行為 |
|---|---|---|---|
| Case 1 | T& / const T& / T*(指標或參考,非 universal reference) |
1. 忽略 expr 的 reference-ness 2. 對 ParamType 做 pattern-match 求 T |
const 保留;reference 剝除 |
| Case 2 | T&&(universal reference) |
lvalue → T 與 ParamType 皆為 lvalue reference;rvalue → 套用 Case 1 規則 | 唯一會依 lvalue/rvalue 區分引數的情境 |
| Case 3 | T(pass-by-value,非指標非參考) |
忽略 reference-ness 後,再忽略 const / volatile |
param 是全新副本,所以 cv 限定被剝除 |
| 特例 | array / function name 傳入 | by-value → decay 成指標;by-reference → 保留真正的 array/function 型別(含大小) | const char[13] vs const char* |
f(expr) → 推導 T 與 ParamType
│
┌───────────────┼─────────────────┐
▼ ▼ ▼
ParamType 是 T&/T* ParamType 是 T&& ParamType 是 T
(非 universal ref) (universal ref) (by-value)
│ │ │
剝 expr 的 ref expr 是 lvalue? 剝 ref,再剝
const 保留於 ├─ 是 → T = 型別& const/volatile
T 或 ParamType │ (ParamType 也是&) (複本可自由修改)
│ └─ 否 → 套 Case 1 │
▼ ▼ ▼
pattern-match lvalue/rvalue array/function
求出 T 在此被區分 會 decay 成指標
Case 1:ParamType 是參考或指標(非 Universal Reference)
規則:先忽略 expr 的 reference-ness,再與 ParamType 比對求出 T。引數的 constness 會被保留——傳 const 物件給 T& 參數是安全的,因為 const 會成為 T 的一部分。
template<typename T>
void f(T& param); // param 是參考
int x = 27; // int
const int cx = x; // const int
const int& rx = x; // 對 x 的 const 參考
f(x); // T = int, param 型別 = int&
f(cx); // T = const int, param 型別 = const int&(const 進入 T)
f(rx); // T = const int, param 型別 = const int&(rx 的 & 被忽略)
若參數改為 const T&,const 已由 ParamType 承擔,就不再需要推入 T:
template<typename T>
void f(const T& param); // param 是 ref-to-const
f(x); // T = int,param 型別 = const int&
f(cx); // T = int,param 型別 = const int&
f(rx); // T = int,param 型別 = const int&
指標參數(T*)行為本質相同:
template<typename T>
void f(T* param); // param 是指標
const int* px = &x;
f(&x); // T = int, param 型別 = int*
f(px); // T = const int, param 型別 = const int*
rvalue reference 參數(真正的 SomeType&&,非模板推導的 T&&)的推導規則與 lvalue reference 完全相同,只是僅接受 rvalue 引數——該限制與型別推導無關。
Case 2:ParamType 是 Universal Reference(T&&)
宣告語法長得像 rvalue reference(T&&),但遇到 lvalue 引數時行為完全不同(完整機制見 Item 24 與 Item 28 的 reference collapsing):
expr是 lvalue → T 與 ParamType 都推導為 lvalue reference(這是模板推導中 T 唯一會是參考的情況)expr是 rvalue → 套用 Case 1 的一般規則
template<typename T>
void f(T&& param); // param 是 universal reference
f(x); // x 是 lvalue → T = int&, param 型別 = int&
f(cx); // cx 是 lvalue → T = const int&, param 型別 = const int&
f(rx); // rx 是 lvalue → T = const int&, param 型別 = const int&
f(27); // 27 是 rvalue → T = int, param 型別 = int&&
只有 universal reference 的推導會區分 lvalue / rvalue 引數;Case 1 與 Case 3 對兩者一視同仁。這正是 perfect forwarding 得以運作的基礎。
Case 3:ParamType 非指標非參考(Pass-by-Value)
param 是傳入物件的全新副本,因此 expr 的 reference-ness、const、volatile 全部被忽略——原物件不可修改,不代表它的副本不可修改。
template<typename T>
void f(T param); // param 以值傳遞
f(x); // T 與 param 都是 int
f(cx); // 仍是 int(const 被剝除)
f(rx); // 仍是 int(& 與 const 都被剝除)
被忽略的只有 expr 本身的 constness(top-level const)。若 expr 是「指向 const 的 const 指標」,指標自身的 const 被剝除,但指標所指之物的 const 保留:
const char* const ptr = "Fun with pointers";
f(ptr); // T = const char*(ptr 自身的 const 消失,
// 指向內容的 const 保留)
特例:Array 與 Function 引數的 Decay
C++ 沒有真正的 array 型別函式參數——void myFunc(int param[]) 會被視為 void myFunc(int* param)。因此:
| 傳入方式 | Array 引數 const char name[13] |
Function 引數 void someFunc(int, double) |
|---|---|---|
by-value(T param) |
T = const char*(decay 成指標) |
T = void (*)(int, double)(函式指標) |
by-reference(T& param) |
T = const char [13],param = const char (&)[13](保留大小) |
param = void (&)(int, double)(函式參考) |
參考可綁定 array 的特性可用來在編譯期取得陣列元素數:
// 以編譯期常數回傳陣列大小(參數無名稱,只在乎元素個數 N)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept // constexpr 見 Item 15
{ // noexcept 見 Item 14
return N;
}
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // 7 個元素
std::array<int, arraySize(keyVals)> mappedVals; // 大小同為 7
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
f(T& param) 傳入 const int& rx |
T = const int,param = const int&(& 剝除、const 保留) |
f(const T& param) 傳入任何 int 變體 |
T 一律 = int(const 由 ParamType 承擔) |
f(T&& param) 傳入 lvalue |
T 與 param 都是 lvalue reference(T 唯一為參考的情況) |
f(T&& param) 傳入字面值 27 |
T = int,param = int&&(rvalue 走 Case 1 規則) |
f(T param) 傳入 const int / volatile int |
T = int(by-value 剝除 cv 限定) |
f(T param) 傳入 const char* const |
T = const char*(只剝頂層 const) |
| array 傳給 by-value 模板參數 | decay 成指標(const char*) |
array 傳給 T& 參數 |
T = 含大小的 array 型別(const char [13]) |
| 函式名稱傳給模板 | by-value → 函式指標;by-reference → 函式參考 |
| 編譯期求陣列大小 | template<typename T, std::size_t N> constexpr std::size_t arraySize(T (&)[N]) noexcept |
| 想知道編譯器推導出什麼型別 | 用 Item 4 的技巧(TD 模板錯誤訊息 / Boost.TypeIndex) |
Related Notes
- 02-Deducing-Types/02-Auto-Type-Deduction — auto 推導幾乎完全沿用本 Item 規則,僅 braced initializer 例外
- 02-Deducing-Types/03-Decltype — decltype 走自己的規則,不剝除參考與 const
- 02-Deducing-Types/04-Viewing-Deduced-Types — 驗證編譯器實際推導結果的工具
- 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References — Case 2 的完整故事:universal reference 的辨識
- 06-Move-Semantics-And-Perfect-Forwarding/06-Reference-Collapsing — lvalue 推導出
T = int&後int& &&如何摺疊成int& - 04-Moving-To-Modern-Cpp/02-Nullptr — 0/NULL 在模板推導中被推成整數型別而非指標的陷阱