本文翻譯自modern effective C++,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!
一些用戶對復雜的系統會忽略它怎么工作,怎么設計的,但是很高興去知道它完成的一些事。通過這樣的方式,c++中的template類型的推導取得了巨大的成功。數以萬計的程序員曾傳過參數給template函數,並得到了滿意的結果。盡管很多那些程序員很難給出比朦朧的描述更多的東西,比如那些被推導的函數是怎么使用類型來推導的。
如果你也是其中的一員,我這有好消息和壞消息給你。好消息是template類型的推導是現代c++最令人驚嘆特性之一(auto)的基礎。如果你熟悉c++98的template類型推導,那么你也會熟悉c++怎么使用auto來做推導類型。而壞消息是當template類型推導規則應用在auto上時,比起應用在template中時,他們有時候看起來會比較難理解。由於這個原因,我們很有必要完全理解template類型推導的各個方面,因為auto是建立在這個基礎上的。這個條款包含了你想知道的東西。
如果你願意瀏覽少許偽代碼,我們可以把函數模板看成這樣子:
template<typename T>
void f(ParamType param);
對於函數的調用看起來是這樣的:
f(expr);
在整個編譯期間,編譯器用expr來推導兩個類型:一個是T ,另一個是
ParamType 。這兩個類型常常是不一樣的,因為ParamType常常包含一些修飾符。(比如const或引用的限定)。比方說,如果一個template聲明成這樣:
template<typename T> void f(const T& param);
然后我們這樣使用它:
int x = 0; f(x);
T被推導成int,但是ParamType被推導成const int&。
很自然會覺得T類型就是傳入函數的實參類型,也就是T 是 expr的類型。 在上面的例子中,x是int,T被推導成int,但是它不總是這樣工作的。 對T類型的推導,不僅僅取決於expr ,同時也取決於ParamType。這里有三種情況:
- ParamType是指針或引用,但不是一個universal引用(universal引用在#24中描述,現在,你要知道的就是,他們存在,並且左引用和右引用是不相同的)
- ParamType是universal引用。
- ParamType不是指針也不是引用
因此我們有三種類型的推導情況需要分析。每一個將建立在下面這個template的基礎上,並如此調用:
template<typename T> void f(ParamType param); f(expr);
情況1:參數是指針或引用,但不是一個universal引用
這種情況是最簡單的情況,類型推導將這么工作:
- 如果expr的類型是引用,忽略引用的部分。
- 然后用expr的類型模式匹配ParamType來決定T。
比方說,下面的template:
template<typename T>
void f(T& param);
並且我們有這些變量聲明:
int x = 27; const int cx = x; const int& rx = x;
對於各種調用,param和T的類型推導是下面這用的:
f(x); //T是int, param的類型是 int& f(cx); //T是const int, param的類型是 cosnt int& f(rx); //T是const int, param的類型是 const int &
注意在第二個和第三個函數調用中,因為cx和rx是const變量,T被推導成cosnt int,因此param類型是const int&。這對調用者是很重要的。當他們傳入const引用對象參數,他們希望對象是不可改動的。也就是,參數要是一個reference-to-const。這也是為什么傳入一個const對象給使用T&參數的template是安全的:對象的const屬性會被推導成T的一部分。
注意在第三個例子中,即使rx的類型是引用,T還是被推導成非引用。這是因為第一點,在推導時,rx的引用屬性被忽略了。
這些例子顯示了左值引用,但是對右值引用的推導和左值引用是一樣的。
如果我們把f的參數從T&改成 const T&,情況將發生小小的變化,但是不會出乎意料。cx和rx的const屬性仍然存在,但是因為我們現在假設param是const引用,所以就沒有必要把const屬性推導到T上去了:
f(x); //T是int, param的類型是 cosnt int& f(cx); //T是int, param的類型是 cosnt int& f(rx); //T是int, param的類型是 cosnt int&
同樣地,rx的引用屬性被忽略了。
如果param是一個指針(或者指向const的指針),情況和引用是一樣的:
template<typename T> void f(T* param); int x = 27; const int *px = &x; f(&x); //T是int, param的類型是int* f(px); //T是cosnt int, param的類型是const int*
現在你可能覺得有些困了,因為對於引用和指針類型,c++的類型推導規則是如此的自然,把他們一個個寫出來是如此枯燥,因為所有的事情顯而易見,就和你想要的推導系統一樣。
情況2:參數是一個universal引用
對於一個采用universal引用參數的template,情況變得復雜。這樣的參數大多被聲明成右值引用(比如在函數模板采用一個類型T,一個universal引用的聲明類型是T&&),但是他們表現的和傳入左值參數時不同。完整的情況將在item 24討論,但是這里給出一些大概內容:
- 如果expr是一個左值,T和param會被推導成左值引用,這里有兩個不尋常點。第一,這是唯一一種T被推導成引用的情況。第二,盡管ParamType在語法上被聲明成一個右值引用,但是他的推導類型是一個左值引用。
- 如果expr是一個右值,適用情況1的規則
舉個例子:
template<typename T> void f(T&& param); int x = 27; const int cx = x; const int& rx = x; f(x); //x是左值,所以T是int&, //param的類型也是int& f(cx); //cx是左值,所以T是const int&, //param的類型也是const int& f(rx); //rx是左值,所以T是const int&, //param的類型也是const int& f(27); //27是右值,所以T是int, //param的類型是int&&
item 24解釋了為什么這個例子會這樣工作。這里的關鍵點是universal引用的類型推導規則對左值和右值是不同的。尤其是,當universal引用在使用中,類型推導會根據傳入的值是左值還是右值進行區分。non-universal引用永遠不會發生這樣的情況。
情況3:參數不是指針也不是引用
當ParamType不是指針也不是引用時,我們處理傳值(by-value)情況:
template<typename T>
void f(T param);
這意味着,param將成為傳入參數的一份拷貝,一個全新的對象。
- 和以前一樣,如果expr的類型是引用,忽略引用屬性。
- 如果,在忽略expr的引用屬性后,expr是const,再忽略const屬性。如果是volatile的,同樣忽略。(volatile對象不常見,他們通常用來實現設備驅動,詳細的情況,在item40中討論)
因此:
int x = 27; cosnt int cx = x; const int& rx = x; f(x); //T和param類型相同,int f(cx); //T和param類型相同,int f(rx); //T和param類型相同,int
記住,盡管cx和rx是const變量,param卻不是const的。這是有意義的,param是一個完全獨立於cx和rx的對象--來自拷貝。事實上,cx和rx能不能被改變和param能不能被改變沒有聯系,這也就是為什么在推導類型的時候,expr的const屬性被忽略了。只是因為expr不能被改變不意味着他們的拷貝不能被改變。
很重要的是,你要知道const(和volatile)屬性被忽略只適用於傳值(by-value)參數。就像我們看到的,在推導類型的時候,傳引用(references-to)或傳指針(pointers-to)參數的const屬性是會保留的。但是我們來考慮一下expr是cosnt指針指向const類型的情況,並且expr是傳值(by-value)參數:
template<typename T> void f(T param); cosnt char* cosnt ptr = "Fun with pointers"; f(ptr); //T是const char,param的類型是const char*
這里,星號右邊的const聲明ptr是const的:ptr不能指向別的地方,也不能設為空。(星號左邊的const意味着ptr指向的一個---字符串---是const的,因此不能被修改)當ptr傳入f,ptr會拷貝一份,賦給param,因此,指針本身(ptr)將以傳值(by-value)形式傳入。同樣的類型推導規則,傳值(by-value)參數ptr的const屬性將會被忽略,所以*param的類型會被推導成cosnt char,一個指向const字符類型的可以改變指向的指針。
數組參數
之前的那些已經設計到大多數主流的template參數推導了,但是,這里還有一些小部分的情況值得我們去了解。數組類型不同於指針類型,盡管有時候它們看起來可以相互替換。這個錯覺主要來自於:在很多時候,一個數組可以退化成(decays)一個指向其第一個元素的指針。這個退化允許代碼寫成這樣:
const char name[] = "J.P.Briggs"; const char* ptrToName = name;
這里,一個const char* 指針ptrToname用const char[13]的數組*name來初始化。這些類型(cosnt char 和 const char[13])不一樣,但是由於array-to-point的退化規則,代碼可以編譯。
但是,當把數組傳值(by-value)傳入函數時會發生什么:
template<typename T> void f(T param); f(name);
我們從函數沒有數組參數開始,是的,是的,這樣的語法是合法的:
void myFunc(int param[]);
但是數組的聲明會被當成指針的聲明,這意味着myFunc能等價地聲明成:
void myFunc(int* param);
這種對於數組和指針參數的等價情況是從C那邊來的,並且這增加了數組和指針相同的錯覺。
因為數組參數聲明被對待成指針參數,通過傳值(by-value)傳入template函數的數組被推導成指針類型。這意味着上面的參數T被推導成const char*:
f(name); //name是數組,但是T被推導成cosnt char*
但是,現在有個問題,盡管函數不能聲明真正的數組參數,但是他們可以聲明指向數組的引用參數!所以,如果我們修改函數為傳引用(by-references):
template<typename T>
void f(T& param);
並且我們傳入一個數組:
f(name);
對T的類型推導將是真正的數組類型!這個類型包含了數組的大小,所以在這個例子中,T被推導成cosnt char[13],並且param的類型(一個引用指向這個數組)是cosnt char(&)[13]。是的,這語法看起來有毒,但是知道這個規則會讓你了解到別人很少注意的點。
有趣的是,聲明指向數組的引用使我們能創造一個template來推導數組的大小:
//在編譯期返回數組大小(數組參數沒有名字,因為我們僅僅為了知道數組的大小) template<typename T, std::size_t N> cosntexpr std::size_t arraySize(T(&)[N]) noexcept { return N; }
在item15中解釋聲明這樣的constexpr的函數可以讓結果在編譯期返回。這使我們可以用一個數組的大小做為另一個新數組的大小來初始化新數組:
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35}; int mappedVals[arraySize(keyVals)];
當然了,作為一個現代C++開發者,你可能更喜歡一個std::array來建立一個數組:
std::array<int, arraySize(keyVals)> mappedValsl;
對於arraySize被聲明為noexcept,這能幫助編譯器產生更好的代碼。想知道細節,可以看item 14。
函數參數
在C++中,數組不是唯一的退化成指針的情況,函數類型也會退化成函數指針,並且所有的我們對數組的推導的討論適用於函數類型推導:
void someFunc(int, double); template<typename T> void f1(T param); template<typename T> void f2(T& param); f1(someFunc); //T是void (*)(int, double) f2(someFunc); //T是 void (&)(int, double)
這看起來幾乎沒有任何不同,但是,如果你已經知道了array-to-pointer的退化,你也會同樣知道function-to-pointer的退化。
現在你明白了吧:函數類型推導的規則,我在開始就說他們相當簡單,並且大多數情況下是這樣的。對於universal引用,左值當推導的類型時需要特別對待,這使我們有點暈暈的,然而退化成指針(decay-to-pointer)的規則使得我們更加暈了。有時,你簡單地想抓住你的編譯器和並渴望:“告訴我你推導的類型是什么”。當發生這樣的事時,轉到item 4,因為這條款專注於欺騙編譯器去做你想它做的事。
你要記住的事
- 在template類型推導的時候,references類型的參數被當成non-references。也就是說引用屬性會被忽略。
- 當推導universal類型的引用參數時,左值參數被特殊對待。
- 當推導傳值(by-value)類型參數時,cosnt 和/或 volatile 參數被當成 non-const 和 non-volatile。
- 當推導類型是數組或函數時會退化成指針,除非形參是引用。