心血來朝的就想翻譯一下Effective Modern C++,非嚴謹翻譯呵晨,大伙兒湊合著看吧。
記得剛接觸C++是考上大學那會兒,為了更好的融入大學生活褂始,特意在那個開學前的暑假騎了一小時的自行車到隔壁鎮(zhèn),風塵仆仆的進到當時鎮(zhèn)上唯一的一家網吧媒至。戰(zhàn)戰(zhàn)兢兢的打開電腦,學會了CS(Counter Strike_)谷徙。學校第一學期就開了C++拒啰,剛摸上電腦就接觸這么高深莫測的語言真事有夠膽戰(zhàn)心驚的,這C++的課一開就是一年完慧,結果一年下來图呢,面向對象啥的聽起來依舊如天書。
先來看看歷史吧
- C++98(哥當年學的)只有一種類型推導:函數(shù)模板
- C++11修改了這個規(guī)則,添加了兩個新的:auto以及decltype
- C++14繼續(xù)擴展了auto及decltype的應用語境
自打上C++14蛤织,這語言就越發(fā)的靈活了赴叹,有太多的場景會出現(xiàn)類型推導,理解類型推導是怎么進行的就變的尤為重要指蚜。
這一章節(jié)會解釋一下模板類型推導是遵循怎么個規(guī)則乞巧,auto/decltype又怎么在這個基礎上進行類型推導。除此之外摊鸡,我們還會教你怎么誘騙編譯器按你想要的結果去工作绽媒,是不是很牛!
Item 1: 理解模板類型推導
無數(shù)的碼農們每天都在愉快的使用著模板類型推導(“理所應當嘛免猾,你你編譯器當然得知道我說的是什么類型了是辕,是吧”),然而對于內部怎么工作就全然不必去關心猎提。
如果你正好是這些愉快碼農中的一員获三,那我這里又一個好消息還有一個壞消息(你要先聽哪一個?)锨苏。好消息是模板的類型推導和auto的類型推導系出同宗疙教,如果你之前用C++98的template用的很愉悅,那么等你切換到C++11以后伞租,auto的類型推導似乎是一樣一樣的贞谓。壞消息是模板類型推導規(guī)則被應用在auto類型推導的場景中時,往往不如模板類型推導那么直觀葵诈,所以有必要去真正理解一下類型推導的規(guī)則裸弦。
先來看一段偽代碼吧,思考這樣的一個函數(shù)模板
template<typename T>
void f(ParamType param);
可以這樣調用這個函數(shù)
f(expr); // call f with some expression
編譯過程中作喘,編譯器依據(jù)expr來推導兩個類型理疙,一個是T,另一個是ParamType. 這兩個類型經常是不一樣的徊都,因為ParamType經常會有一個比如說const的修飾符。
來看個例子广辰,如下的定義模板函數(shù)
template<typename T>
void f (const T& param); // param type is const T&
并且這樣調用這個函數(shù)
int x = 0;
f(x); //call f with an int
T被推導成int暇矫,ParamType推導成int&
這個例子里面T的類型就是函數(shù)入?yún)xpr的類型,x是int择吊,自然能推導出T的類型也是int李根。但是并不是所有的時候都這樣,T的類型推導有時候不單單取決于函數(shù)入?yún)xpr的類型几睛,它還依賴于ParamType的類型房轿。有如下三種場景
- ParamType是一個指針或者引用類型,但不是一個全局引用(全局引用在item24中有描述。這會兒你只要知道有這樣一種引用囱持,它區(qū)別于左值引用或右值引用)
- ParamType是一個全局引用
- ParamType既不是指針也不是引用
我們這里有三種類型推導的場景夯接,都以如下的方式定義函數(shù)模板并調用
template<typename T>
void f (ParamType param);
f(expr); // deduce T and ParamType from expr
Case 1: ParamType是一個引用或指針,但不是全局引用
最簡單的場景是ParamType是一個引用或者指針類型纷妆,但不是全局引用盔几。這個時候類型推導是這么工作的
- 如果函數(shù)參數(shù)expr的類型是引用,忽略參數(shù)的引用特性
- 通過匹配expr的類型掩幢,獲取ParamType的類型進而確定T的類型
例如逊拍,這是我們的函數(shù)模板
template <typename T>
void f(T& param); // param is a reference
我們定義如下的變量
int x = 27; // x is an int
const in cx = x; // cx is an const int
const int& rx = x; //rx is a reference to a const int
當函數(shù)被調用時,類型推導成下面這樣
f(x); // T is int, param`s type is int&
f(cx); // T is const int, param`s type is const int&
f(rx); // T is const int, param`s type is const int&
第二個和第三個函數(shù)調用中际邻,由于cx和rx指定成const類型芯丧,推導出T是const int,從而產生了參數(shù)類型是const int&世曾。 這一點對于函數(shù)調用者來說很重要缨恒。當傳 遞一個const對象給一個引用類型的參數(shù),函數(shù)調用者期望這個對象維持const特性(不可以修改)度硝。例如肿轨,期望這個函數(shù)參數(shù)是一個const引用。這就是為什么傳遞一個const對象給這樣的模板函數(shù)(攜帶T&參數(shù))是安全的蕊程,對象常量性也成為了類型T推導的一部分了椒袍。
第三個例子中,即使rx的類型是一個引用藻茂,推導出來T的類型依舊是一個非引用類型(non-reference)驹暑。這是由于在類型推導過程中,rx的引用性(reference-ness)被忽略了辨赐。
上述的這些例子都是左值引用類型优俘,但是類型推導的規(guī)則對于右值引用參數(shù)同樣有用。當然掀序,只有右值實參能傳遞給右值引用參數(shù)帆焕,但是這個限制不會影響類型推導
如果我們修改了函數(shù)f的參數(shù),從T&變成constT&不恭,這時候發(fā)生了一點點改變叶雹。cx和rx的常量性依舊會得以保留。但是這個時候類型T就不會再有const特性了(不需要推導成const類型了)换吧。
template <typename T>
void f(const T& param); // param is now a ref-to const
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T is int, param`s type is const int&
f(cx); // T is int, param`s type is const int&
f(rx); // T is int, param`s type is const int&
和之前一樣折晦,rx的引用性(reference-ness)在類型推導的過程中忽略了
如果參數(shù)是變成了指針(或是一個指向const的指針),類型推導依舊遵循同樣的規(guī)則
template<typename T>
void f (T* param); //param is now a pointer
int x = 27; // as before
const int *px = &x; // px is a ptr to x as a const int
f(&x); // T is int, param`s type is int*
f(px); // T is const int, param`s type is const int*
開始打盹兒了吧沾瓦,因為c++的類型推導規(guī)則看起來是那么的理所應當?shù)穆牛蠹視茏匀坏恼J為類型推導不就應該是那樣的么谦炒。但當我們真正的一條條羅列出來所以然的時候一下就變的好枯燥了。
Case 2: ParamType是一個全局引用
對于模板函數(shù)參數(shù)是全局引用的場景(T&&)风喇,類型推導就不是那么顯而易見了宁改。這些參數(shù)往往被聲明稱右值引用(例如,一個函數(shù)模板的入?yún)㈩愋蚑响驴,一個全局引用的聲明方法是T&&)透且,當左值參數(shù)傳遞進來時,這兩種函數(shù)模板的行為是不一樣的豁鲤。在Item24中會詳細描述秽誊,這里概述一下
- 如果expr是一個左值,T和Paramtype都被推導成為左值引用
- 如果expr是一個右值琳骡,使用通常情況下的推導規(guī)則
舉個例子
template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // x is lvalue, so T is int &
// param`s type is also int&
f(cx); // cx is lvalue, so T is const int &
// param`s type is also const int &
f(rx); // rx is lvalue, so T is const int&
// param`s type is also const int&
f(27); // 27 is rvalue, so T is int
// param`s type is int&&
很明顯當使用全局引用的時候锅论,類型推導區(qū)分左值參數(shù)和右值參數(shù)。對于non-universal引用來說楣号,這是從未有過的最易,Item 24會詳細的解釋這個原因。
Case 3: ParamType既不是指針也不是引用
這里我們來說說傳值調用
template<typename T>
void f(T param); //param is now passed by value
這里param是傳入值的一個拷貝炫狱,一個全新的對象藻懒。param是一個全新對象的事實驅動T的類型推導規(guī)則
- 和之前一樣,如果expr是引用類型视译,忽略入?yún)⒌囊锰匦裕╮eference-ness)
- 而后如果expr是const類型嬉荆,一并忽略。如果是volatile類型酷含,繼續(xù)忽略(volatile不常用鄙早,詳細參看Item40)
所以
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x) // T`s and param`s type are both int
f(cx) // T`s and param`s type are again both int
f(rx) // T`s and param`s type are still both int
注意到這里cx和rx雖然代表const值,但param是全新的對象(cx或rx的一個拷貝)椅亚,它不是const限番,這就說的通了。這就是為啥expr的這些特性(constness/volatileness/etc.)在類型推導的過程中都被忽略了
這里要記住只有傳值參數(shù)的時候才會忽略這些const等等呀舔。但是當考慮這么個case弥虐,expr是一個指向const對象的const指針,然后expr按值傳遞進函數(shù)媚赖。如下霜瘪,
template<typename T>
void f(T param); // param is still passed by value
const char* const ptr = "Fun with pointers" // ptr is const pointer to const object
f(ptr); // pass arg of the type const char* const
ptr這里是一個const指針省古,不能指向別的地方了粥庄,同樣也不能設置成null丧失。當ptr作為函數(shù)調用參數(shù)時豺妓,指針自身(ptr)會按值傳遞,指針(string的地址)復制到了param。ptr的常量性(constness)會被忽略掉琳拭,這時候param的類型推導出來是const char*训堆,新的指針param可以指向不同的位置了,但是當前param指向的內容是不能改變的(這也很顯而易見的)
數(shù)組作為參數(shù)
數(shù)組類型有別于指針類型白嘁,雖然它們有時候看起來可以互換坑鱼。造成這種假象的原因是,很多場景下絮缅,數(shù)組會退化成指向數(shù)組頭的指針鲁沥。正因為有這種退化,使得下面代碼能編譯通過
const char name[] = "J.P.Briggs" // name`s type is const char[13]
char char * ptrToName = name; // arrary decays to pointer
這里的ptrToName被初始化成name耕魄,name是一個const類型的數(shù)組画恰。
但是當傳遞一個數(shù)組給傳值調用的模板函數(shù)的時候會發(fā)生些啥?參看下面的偽代碼吸奴。
template<typename T>
void f(T param); // template with by-value parameter
f(name); // what types a deduced for T and param?
發(fā)現(xiàn)了沒允扇,函數(shù)的參數(shù)好像并沒有數(shù)組類型嘛!
我們來看一個看起來有點兒像數(shù)組類型作為入?yún)⒌睦釉虬隆O旅娴暮瘮?shù)定義就是合法的考润。
void myFunc(int param[]);
但是這里的參數(shù)param是被認作為一個指針的,意味著myFunc和下面定義的函數(shù)是等價的
void myFunc(int* param); // same function as above
正是有上述例子的存在读处,才使得數(shù)組和指針等價這個假象得以被很多人接受糊治。
由于數(shù)組參數(shù)聲明退化成指針參數(shù),當數(shù)組作為一個值傳遞給一個模板函數(shù)档泽,推導出來的類型應該是指針類型俊戳,意味著下面的代碼中T被推導成const char*。
f(name); // name is array, but T deduced as const char*
接下來我們有一種曲線救國的方法(見證奇跡的時刻)馆匿,雖然函數(shù)不能聲明一個數(shù)組類型的參數(shù)抑胎,但是可以聲明一個數(shù)組的引用類型參數(shù)。
template<typename T>
void f(T& param); // template with by-reference parameter
然后傳遞一個數(shù)組給這個函數(shù)
f(name); // pass array to f
這個時候T的類型就真正的變成一個array渐北。這個類型還隱含了數(shù)組的大小阿逃。這個例子里面f的參數(shù)類型是const char(&)[13].
有意思的是,聲明一個指向數(shù)組的引用使得我們可以創(chuàng)建這樣一個模板函數(shù)赃蛛,這個模板可以推導一個數(shù)組包含的元素個數(shù)恃锉。
// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template <typename T, std::size_t N>
constexprstd::size_t arraySize(T (&) [N]) noexcept
{
// see info below on constexpr and noexcept
return N
}
正如Item15中描述的,聲明這樣的函數(shù)constexpr呕臂,使得在編譯過程中就能獲得函數(shù)運行結果破托。所以下面的代碼實現(xiàn)就變的可行了,我們可以定義一個新的數(shù)組歧蒋,這個數(shù)組的大小和另一個數(shù)組一樣土砂。
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35}; //keyVals has 7 elements
int mappedVals[arraySize(keyVals)]; // so does mappedVals
當然州既,你可能更加喜歡std::array來定義數(shù)組。
std::array<int, arraySize(keyVals)> mappedVals萝映;// mappedVals size is 7
函數(shù)作為參數(shù)
C++里面吴叶,函數(shù)同樣也可以退化成函數(shù)指針,前面討論的那些類型推導規(guī)則這里同樣適用 序臂。
void someFunc(int, double); // someFunc is a function;
// type is void(int, double)
template<typename T>
void f1(T param) ; // in f1, param is passed by value
template<typename T>
void f1(T& param); // in f2, param passed by ref
f1(someFunc); // param deduced as ptr-to-func
// type is void(*)(int, double)
f2(someFunc); // param deduced a ref-to-func;
// type is void(&)(int, double)
到這兒你就知道這些模板類型推導的規(guī)則了蚌卤,所以吧,這些規(guī)則看上去就是這么的簡單直接奥秆。唯一的污點就是universal references場景下的左值參數(shù)逊彭,還有退化為指針的規(guī)則。那能不能更簡單點兒构订,抓住編譯器然后命令它“你都給我推導成啥啥類型诫龙?”,看看Item 4吧鲫咽,你會找到答案的
記住以下幾點
- 模板類型推導時签赃,忽略引用類型參數(shù)的引用性(reference-ness)
- 給universal reference參數(shù)進行類型推導時,左值要特別對待
- 傳值參數(shù)的類型推導分尸,入?yún)⒌闹T如所有const /volatile的特性都會忽略
- 模板板類型推導過程中锦聊,數(shù)組或函數(shù)做微參數(shù)時會退化成指針,除非模板函數(shù)的參數(shù)是引用類型