Item 1: 理解模板類型推導

心血來朝的就想翻譯一下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是一個引用或者指針類型纷妆,但不是全局引用盔几。這個時候類型推導是這么工作的

  1. 如果函數(shù)參數(shù)expr的類型是引用,忽略參數(shù)的引用特性
  2. 通過匹配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ī)則

  1. 和之前一樣,如果expr是引用類型视译,忽略入?yún)⒌囊锰匦裕╮eference-ness)
  2. 而后如果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ù)是引用類型
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末箩绍,一起剝皮案震驚了整個濱河市孔庭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌材蛛,老刑警劉巖圆到,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卑吭,居然都是意外死亡芽淡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門豆赏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挣菲,“玉大人,你說我怎么就攤上這事掷邦“渍停” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵抚岗,是天一觀的道長或杠。 經常有香客問我,道長宣蔚,這世上最難降的妖魔是什么向抢? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任蔓涧,我火速辦了婚禮,結果婚禮上笋额,老公的妹妹穿的比我還像新娘。我一直安慰自己篷扩,他們只是感情好兄猩,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鉴未,像睡著了一般枢冤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铜秆,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天淹真,我揣著相機與錄音,去河邊找鬼连茧。 笑死核蘸,一個胖子當著我的面吹牛,可吹牛的內容都是我干的啸驯。 我是一名探鬼主播客扎,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼罚斗!你這毒婦竟也來了徙鱼?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤针姿,失蹤者是張志新(化名)和其女友劉穎袱吆,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體距淫,經...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡绞绒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榕暇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片处铛。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖拐揭,靈堂內的尸體忽然破棺而出撤蟆,到底是詐尸還是另有隱情,我是刑警寧澤堂污,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布家肯,位于F島的核電站,受9級特大地震影響盟猖,放射性物質發(fā)生泄漏讨衣。R本人自食惡果不足惜换棚,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望反镇。 院中可真熱鬧固蚤,春花似錦、人聲如沸歹茶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惊豺。三九已至燎孟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尸昧,已是汗流浹背揩页。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烹俗,地道東北人爆侣。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像幢妄,于是被迫代替她去往敵國和親累提。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內容