Item 1:理解模板型別推導 (Understand Template Type Deduction)

Overview Table

模板呼叫 f(expr) 時,編譯器由 expr 同時推導 TParamTypeparam 的完整型別)。推導結果取決於 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*
Tip

rvalue reference 參數(真正的 SomeType&&,非模板推導的 T&&)的推導規則與 lvalue reference 完全相同,只是僅接受 rvalue 引數——該限制與型別推導無關。

Case 2:ParamType 是 Universal Reference(T&&)

宣告語法長得像 rvalue reference(T&&),但遇到 lvalue 引數時行為完全不同(完整機制見 Item 24 與 Item 28 的 reference collapsing):

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&&
Important

只有 universal reference 的推導會區分 lvalue / rvalue 引數;Case 1 與 Case 3 對兩者一視同仁。這正是 perfect forwarding 得以運作的基礎。

Case 3:ParamType 非指標非參考(Pass-by-Value)

param 是傳入物件的全新副本,因此 expr 的 reference-ness、constvolatile 全部被忽略——原物件不可修改,不代表它的副本不可修改。

template<typename T>
void f(T param);         // param 以值傳遞

f(x);    // T 與 param 都是 int
f(cx);   // 仍是 int(const 被剝除)
f(rx);   // 仍是 int(& 與 const 都被剝除)
例外:只剝除「頂層 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-valueT param T = const char*(decay 成指標) T = void (*)(int, double)(函式指標)
by-referenceT& 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)