模板是C++的重要特性,是C++標準模板庫的基礎。模板可以根據數據類型自動生成代碼,大大減少重復代碼。模板實例化的時候編譯器需要根據具體變量推導數據類型,模板推導出的類型很多時候是顯而易見的,有些時候卻不太明顯,本文詳細闡述一下C++模板的類型推導機制。
在C++中聲明一個模板函數的偽代碼如下:
template<typename T>
void f(ParamType param);
上面的ParamType
與T
不一定相同,比如:
template<typename T>
void f(const T& param);
此時ParamType
的類型是const T&
。
調用模板函數方式如下:
f(expr);
編譯的時候,編譯器通過表達式expr
推導出兩個類型ParamType
與T
。
模板的類型推導與ParamType
密切相關,根據ParamType
的類型可以分為三種情形:
- ParamType 既不是指針也不是引用。
- ParamType 是指針或引用,不是通用引用。
- ParamType 是通用引用。
下面分別討論一下三種情形:
ParamType 既不是指針也不是引用
即ParamType
與T
相同,此時模板如下:
template<typename T>
void f(T param);
這種情況下參數param
的類型T
可以理解為值傳遞(param
會復制一份expr
)時的類型。這意味著:
- 如果
expr
是一個引用,忽略引用部分。 - 如果
expr
帶有const
或volatile
,忽略const
、volatile
。
舉個例子:
int x = 27;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);
上面的三種調用方式,T
的類型都是int
。
需要注意下面一種情況:
const char* const ptr = "Fun with pointers";
f(ptr);
ptr
是指向字符串常量的常量指針,此時進行值傳遞T
的類型應為const char*
。因為:
- 開頭的
const
修飾的是指向的對象,表示指向的字符串不可變,不可忽略,否則指向的類型就不對了。 -
*
右邊的const
修飾的是指針,表示指針本身不可變,在值傳遞的情況下指針是被復制一份,該const
沒有意義。
ParamType 是指針或引用,不是通用引用
這種情況下,推導步驟如下:
- 如果
expr
是一個引用,忽略引用部分。 - 將
expr
類型與ParamType
進行模式匹配,先確定ParamType,再根據ParamType
推導T
。
舉個例子:
template<typename T>
void f(T& param); // 參數是引用類型
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x是int類型,ParamType類型是int&,所以T是int類型
f(cx); // cx是const int類型,ParamType類型是const int&,所以T是const int類型
f(rx); // rx是const int&類型,忽略引用部分,同cx,ParamType類型是const int&,所以T是const int類型
如果把參數類型改為const T&
,相應的T
的類型也會有一點變化:
template<typename T>
void f(const T& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x是int類型,ParamType類型是const int&,所以T是int類型
f(cx); // cx是const int類型,ParamType類型是const int&,所以T是int類型
f(rx); // rx是const int&類型,忽略引用部分,同cx,ParamType類型是const int&,所以T是int類型
如果參數是指針類型,也類似:
template<typename T>
void f(T* param); // 參數是指針類型
int x = 27;
const int *px = &x;
f(&x); // &x是int*類型,ParamType類型是int*,所以T是int類型
f(px); // px是const int*類型,ParamType類型是const int*,所以T是const int類型
ParamType 是通用引用
這種情況下,推導規則如下:
- 如果
expr
是左值,那么T
和ParamType
都是左值引用。這條規則很特殊:首先,這是唯一一種T
被推導為引用類型的情形;其次,ParamType
的聲明形式是右值引用的語法,但是實際類型為左值引用。 - 如果
expr
是右值,推導規則與普通引用一致。
舉例:
template<typename T>
void f(T&& param); // 參數是通用引用
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x 是左值,所以T和ParamType都是int&類型
f(cx); // cx 是左值,所以T和ParamType都是const int&類型
f(rx); // rx 是左值,所以T和ParamType都是const int&類型
f(27); // 27 是右值,ParamType是int&&類型,所以T是int類型
以上三種情形就是C++模板類型推導的全部規則了。下面簡單補充說明一下C++里兩種特殊的參數類型:數組參數和函數參數。
數組參數
在C語言和C++里,如下的兩個函數聲明是完全等價的:
void myFunc(int param[]);
void myFunc(int* param);
即函數的數組參數會被當成指針參數。
如果要使用真正的數組參數,在C++中可以使用數組引用類型:
void myFunc(int (¶m)[5]); // 使用數組引用時必須指定數組大小,同定義數組一樣
int (¶m)[5]
表示一個大小為5的整數數組引用類型。
了解數組參數的以上特點后,可以很容易的理解模板如何推導數組類型的參數。舉兩個例子:
template<typename T>
void f(T param);
const char name[] = "J. P. Briggs";
f(name); // name是數組,param是值傳遞, 數組參數當成指針參數,因此param的類型是const char*
template<typename T>
void f(T& param);
const char name[] = "J. P. Briggs";
f(name); // name是數組,param是引用類型,因此param的類型是const char(&)[13]
函數參數
除了數組參數,函數參數也會被當成函數指針。函數參數的類型推導規則跟數組參數完全一樣。
void someFunc(int, double);
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc); // param是值傳遞, 函數參數當成函數指針,因此param的類型是void (*)(int, double)
f2(someFunc); // param是引用類型,因此param的類型是void (&)(int, double)
實際使用中函數指針和函數引用基本沒有區別。兩者調用時都可以解引用或直接調用,唯一的區別是引用初始化時只能用函數名稱,不能在前面加&。
void (*pf)(int) = someFunc; // 也可以寫成 void (*pf)(int) = &someFunc;
void (&rf)(int) = someFunc;
pf(8, 1.2); // 也可以寫成 (*pf)(8, 1.2);
rf(8, 1.2); // 也可以寫成 (*rf)(8, 1.2);
總結:
- ParamType 既不是指針也不是引用時,采用值傳遞模式,忽略表達式的引用部分、const、volatile。
- ParamType 是指針或引用,不是通用引用時,忽略引用部分,進行模式匹配,先確定ParamType,再推導T。
- ParamType 是通用引用時,左值特殊對待(T和ParamType都是左值引用)。
- 數組參數、函數參數非引用傳遞時當作指針,引用類型不會。