Effective Modern C++翻譯(2)-條款1:明白模板類型推導

第一章 類型推導程序員

C++98有一套單一的類型推導的規則:用來推導函數模板,C++11輕微的修改了這些規則而且增長了兩個,一個用於auto,一個用於decltype,接着C++14擴展了auto和decltype可使用的語境,類型推導的廣泛應用將程序員從必須拼寫那些顯然的,多餘的類型的暴政中解放了出來,它使得C++開發的軟件更有彈性,由於在某處改變一個類型會自動的經過類型推導傳播到其餘的地方。編程

 

然而,它可能使產生的代碼更難觀察,由於編譯器推導出的類型可能不像咱們想的那樣顯而易見。數組

 

想要在現代C++中進行有效率的編程,你必須對類型推導操做有一個紮實的瞭解,由於有太多的情形你會用到它,在函數模板的調用中,在auto出現的大多數場景中,在decltype表達式中,在C++14,神祕的decltype(auto)構造被應用的時候。安全

 

這一章提供了一些每個C++開發者都須要瞭解的關於類型推導的基本信息,它解釋了模板類型推導是如何工做的,auto是如何在此基礎上創建本身的規則的,decltype是如何按本身的獨立的規則工做的,它甚至解釋了你如何強迫編譯器來使類型推導的結果可見,從而讓你肯定編譯器的結果是你想要的。app

 

條款1 明白模板類型推導函數

聽說模仿是最誠懇的恭維之道,可是充滿喜悅的無知也一樣是能夠衷心讚美的,當使用一個複雜的系統,忽視了它的系統是如何設計的,是如何工做的,然而對它的所完成的事情你依舊會感到很高興,經過這種方式,C++中模板的類型推導成爲了一個巨大的成功,數百萬的程序員向模板函數中傳遞參數,並得到徹底使人滿意的答案,儘管不少程序員被牢牢逼着的去付出比對這些函數是如何被推導的一個朦朧的描述要更多。(even though many of those programmers would be hard-pressed to give more than the haziest description of how the types used by those functions were deduced.)學習

 

若是上面提到數百萬的程序員中包括了你,我有一個好消息也有一個壞消息,好消息是對於auto聲明的變量的類型推導規則和模板在本質上是同樣的,因此當涉及到auto的時候,你會感到很熟悉,壞消息是當模板類型推導的規則應用到auto的時候,你極可能對發生的事情感到驚訝,若是你想要使用auto(相比精確的類型聲明,你固然更應該使用auto,見條款5),你須要對模板類型推導規則有一個合理正確的認識,他們一般是直截了當的,因此這不會照成太大的挑戰,和在C++98裏工做的方式是同樣的,你極可能不須要對此有過多的思考。spa

 

若是你願意忽略少許的僞代碼,咱們能夠直接對下面的模板函數的代碼進行思考。設計

template<typename T> 
void f(ParamType param); 

函數調用像下面這樣指針

f(expr); // 用一些表達式調用f

 

在編譯期間,編譯器使用expr來推導兩個類型:一個是T的,一個是ParamType的,這兩個類型常常是不一樣的,由於ParamType常常包括了一些修飾符,好比const或者是引用的限定,例如,若是模板像下面這樣聲明:

template<typename T> 
void f(const T& param); //在這個例子中, ParamType的類型是是const T& 
int x = 0; 
f(x);                   // 用一個int類型調用函數f

T被推導爲int,可是ParamType被推導爲const int&

 

咱們很天然的去期待推導出的T的類型和傳遞給函數實參的類型是一致的,例如,T的類型就是expr的類型,在上面的例子中,就是這種狀況,x的類型是int,T被推導爲int,當時它並不老是這樣是的,被推導出的T的類型,不只僅取決於expr的類型,一樣取決於ParaType的形式,總共有三種情形;

  • ParamType是一個指針或是一個引用,但不是一個萬能引用(universal reference),(universal reference會在條款26中進行描述,如今你只要知道他是存在的便可)。
  • ParamType是一個萬能引用(universal reference) 。
  • ParamType既不是一個指針也不是一個引用 。

 

 

所以,咱們會有三種類型推導的情景,每個調用都會以咱們通用的模板形式爲基礎:

template<typename T> 
void f(ParamType param); 
f(expr); // 從expr中推導出T和ParamType的類型

 

第一種狀況:ParamType是一個指針或是一個引用,但不是一個萬能引用(universal reference)

最簡單的狀況是當ParamType是一個指針或是一個引用,但不是一個萬能引用(universal reference),在這種狀況下,模型推導的方式會像下面這樣:

  • 若是expr的類型是一個引用,忽略引用的符號
  • 經過模式匹配expr的類型來決定ParamType的類型從而決定T的類型(Pattern-match expr's type against ParamType to determine T)

 

例如,若是咱們的模板函數是這樣的

template<typename T> 
void f(T& param);

咱們有這樣的變量聲明

int x = 27;         // x是int類型
const int cx = x;   // cx是const int 
const int& rx = x;  // rx是x的一個常量引用(rx is a read-only view of x)

函數調用時,推導出的Param和T的類型以下

f(x);    // T是int, param的類型是是int& 
f(cx);   // T是const int, 
         // param的類型是是const int& 
f(rx);   // T是const int, 
         // param的類型是const int&

在第二個和第三個函數調用中,注意到由於cx和rx被指派爲const了,T被推導爲const int,所以產生的參數類型是const int&,這對調用者來講是十分重要的,當他們向一個引用類型的參數傳遞一個const對象時,他們期待這個對象依舊是沒法被修改的,好比,這個參數的類型被推導爲一個指向const的引用,這就是爲何向帶有一個T&參數的模板傳遞一個const對象是安全的,對象的常量性(constness)成爲了推導出的類型T的一部分。

 

在第三個例子中,注意到儘管rs是一個引用類型,T被推導爲一個非引用類型,這是由於rs的引用性(reference-ness)在推導的過程當中被忽略了,若是不是這樣的話(例如,T被推導爲const int&),param的類型將會是const int&&,一個引用的引用,引用的引用在C++裏是不容許的,避免他們的惟一方法在類型推導時忽略表達式的引用性(reference-ness)。

 

這些例子都是左值的引用參數,可是這些類型推導規則對於右值的引用參數同時適用,固然,只有右值的實參會被傳遞給一個右值類型的引用,可是這對類型推導沒有什麼影響。

 

若是咱們把f的參數類型由T&改爲const T&,事情會發送一點小小的改變,但不會太讓人驚訝,cx和rx的常量性依舊知足,可是由於咱們如今假定了param是一個常量的引用,const不在須要被推導爲T的一部分了。

template<typename T> 
void f(const T& param); // param如今是一個指向常量的引用 
int x = 27;             // 和以前同樣 
const int cx = x;       // 和以前同樣 
const int& rx = x;      // 和以前同樣 
f(x);                   // T 是int, param的類型是const int& 
f(cx);                  // T是int, param的類型是const int& 
f(rx);                  // T是int, param類型是const int&

像以前同樣,rs的引用性(reference-ness)在類型推導時被忽略了。

 

若是param是一個指針(或是一個常量指針(point to const))而不是一個引用,規則依舊適用

template<typename T> 
void f(T* param);     // param如今是一個指針 
int x = 27;           // 和以前同樣 
const int *px = &x;   // px是x的一個常量引用(rx is a read-only view of x) 
f(&x);                // T是int, param的類型是int* 
f(px);                // T是const int, 
                      // param的類型是const int*,

此時此刻,你可能發現你本身在不斷的打哈欠和點頭,應爲C++的類型推導規則對於引用和指針類型的參數是如此的天然,看見他們一個個被寫出來是一件很枯燥的事情,由於他們是如此的顯而易見,和你在類型推導中期待的是同樣的。

 

第二種狀況:ParamType是一個萬能的引用(Universal Reference)

當涉及到萬能引用(universal reference)做爲模板的參數的時候(例如 T&&參數),事情變得不是那麼清楚了,由於規則對於左值參數有着特殊的對待,完整的故事將在條款26中講述,但這裏有一個概要的版本。

  • 若是expr是一個左值,T和ParamType都被推導爲一個左值的引用
  • 若是expr是一個右值,使用一般狀況下的類型推導規則

 

例如

template<typename T> 
void f(T&& param);     // param如今是一個萬能引用(universal reference) 
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是一個lvalue, 因此T是const int&, 
                       // param的類型也是const int& 
f(27);                 // 27是一個rvalue, 因此T是int, 
                       // param類型是int&&

條款26精確的介紹了爲何這些例子會是這樣,但關鍵是類型推導對於模板的參數是萬能引用(univsersal references)和參數是左值或右值時規則是不一樣的,當使用萬能引用(univsersal references)的時候,類型推導規則會區別左值和右值,而這歷來不會發生在非萬能(例如,普通)的引用上。

 

第三種狀況:ParamType的類型既不是指針也不是引用

當ParamType的類型既不是指針也不是引用的時候,咱們是按照傳值的方式進行處理的

template<typename T> 
void f(T param);    // param如今是按值傳遞的

這意味着param將會是傳遞過來的對象的一個拷貝,一個全新的對象,事實上,param是一個全新的對象控制導出了T從expr中推導的規則

  • 像以前同樣,如何expr的類型是一個引用,忽略引用的部分
  • 若是在expr的引用性被忽略以後,expr帶有const修飾,忽略const,若是帶有volatile修飾,一樣忽略(volatile對象是不尋常的對象,他們一般僅被用來實現設備驅動程序,更多的細節,能夠參照條款42)

 

所以

int x = 27;        // 和以前同樣
const int cx = x;  // 和以前同樣 
const int& rx = x; // 和以前同樣 
f(x);              // T和param都是int 
f(cx);             // T和param都是int 
f(rx);             // T和param都是int

注意到即便cx和rx表明了常量的對象,param也並非常量的,這是講的通的,由於parm是和cx,rx徹底獨立的對象,它是cx和rx的一個拷貝,事實上cx和rx不能被修改和param是否能被修改沒有任何的關係,這就是爲何expr的常量性在推導param類型的時候被忽略了,由於expr不能被修改並不意味着它的拷貝也不能被修改。

 

注意到const僅僅在按值傳遞的參數中被忽略掉是很重要的,像咱們看到的那樣,對於指向常量的引用和指針來講,expr的常量性在類型推導的時候是被保留的,可是考慮下面的狀況,expr是一個指向const對象的常量指針,而且expr按值傳遞給一個參數,

 

template<typename T> 
void f(T param);       // param是按值傳遞的
const char* const ptr ="Fun with pointers"; 
                       // ptr是一個指向常量對象的常量指針ptr is const pointer to const object 
f(ptr);                // 實參類型是const char * const

這裏,乘號右側的const將ptr聲明爲const意味着ptr不能指向一個不一樣的位置,也不能把它設爲null(乘號左側的const指ptr指向的字符串是const,所以字符串不能被修改),當ptr別傳遞給f的時候,指針按位拷貝給param,所以,指針自己(ptr)將是按值傳遞的,根據按值傳遞的類型推導規則,ptr的常量性將被忽略,param的類型被推導爲const char*,一個能夠修改所指位置的指針,但指向的字符串是不能修改的,ptr所指的常量性在類型推導的時候被保留了下來,可是ptr自己的常量性在經過拷貝建立新的指針param的時候被忽略掉了。

 

數組參數

上面這些已經覆蓋了模板類型推導的主流部分,可是還有一些邊邊角角的地方值得咱們瞭解,數組的類型和指針的類型是有不一樣的,即便他們有的時候看起來是能夠互相交換的,這個錯覺的主要貢獻來源於此,在不少環境中,數組會退化爲指向數組第一個元素的指針,這種退化容許下面的代碼經過編譯。

const char name[] = "J. P. Briggs"; // name的類型是 
                                         // const char[13] 
const char * ptrToName = name;      // 指向數組的指針

這裏,const *char的指針ptrToName被name實例化,而name的類型是const char[13],一個13個元素的常量數組,兩者的類型(const char*和const char[13])是不一樣的,可是由於存在數組到指針間的退化規則,上面的代碼是能夠經過編譯的。

 

可是若是數組經過傳值的方式傳遞給一個模板的時候,會發生什麼呢?

template<typename T> 
void f(T param);      // 模板是參數是按值傳遞的

f(name);              // T的推導結果是?

咱們首先應該注意到函數的參數中是不存在數組類型的參數的,是的,下面的語法是合法的

void myFunc(int param[]);

可是這個數組的聲明是被按照一個指針的聲明而對待的,這意味着myFunc和下面的聲明是等價的

void myFunc(int* param); // 和上面的函數是同樣的

數組和指針在參數上的等價源於C++是以C爲基礎建立的,它產生了數組和指針在類型上是等價的這一錯覺。

 

由於數組參數的聲明被按照指針的聲明而對待,經過按值的方式傳遞給一個模板參數的數組將被推導爲一個指針類型,這意味着在下面這個模板函數f的調用中,參數T的類型被推導爲const char*

f(name); // name是一個數組,可是T被推導爲const char*

可是如今來了一個曲線球,儘管函數不能聲明一個真正意義上的數組類型的參數,可是他們能夠聲明一個指向數組的引用,因此若是咱們把模板f改爲按引用傳遞參數

template< typename T>
void f(T& param); // 模板的參數是按引用傳遞的

如今咱們傳遞數組過去

f(name); // 向f傳遞一個數組

類型T的類型被推導爲數組的類型,這個類型包括了數組的大小,因此在上面這個例子中,T被推導爲const char[13],f的參數的類型(對數組的一個引用)是const char(&)[13],是的,這個語法看起來是有毒的(looks toxic),可是從有利的方面看,知道這些將會獎勵你那些別人得不到的罕見的分數(knowing it will score you mondo points with those rare souls who care)。

 

有趣的是,聲明一個指向數組的引用可以讓咱們建立一個模板來返回數組的長度。

template<typename T, std::size_t N>        // 在編譯期間
constexpr std::size_t arraySize(T (&)[N])  // 返回一個數組
{                                          // 的大小
return N;                                  // N是一個常量
}

注意到constexpr的使用(參見條款14)讓函數的結果在編譯期間就能夠得到,這就可讓咱們聲明一個數組的長度和另外一個數組的長度同樣

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals有
                                            // 7元素
int mappedVals[arraySize(keyVals)];         // mappedVals
                                            // 也是這樣

固然,做爲一個現代C++的開發人員,你應該很天然的使用std::array而不是內置的數組

std::array<int, arraySize(keyVals)> mappedVals; // mappedVals'
                                                // 大小是7

函數參數

數組不是C++中惟一一個能夠退化爲指針的實體,函數類型也能夠退化爲指針,咱們討論的任何一個關於類型推導的規則和對數組相關的事情對於函數的類型推導也適用,函數類型會退化爲函數的指針,所以

void someFunc(int, double); // someFunc是一個函數;
                            // 類型是void(int, double)
template<typename T>
void f1(T param);           // 在函數f1中,參數是按值傳遞的
template<typename T>
void f2(T& param);          // 在函數f2中,參數是按引用傳遞的
f1(someFunc);               // 參數被推導爲指向函數的指針                            // 類型是void (*)(int, double)
f2(someFunc);               // 參數被推導爲指向函數的引用
                            // 類型是void (&)(int, double)

事實上,這和數組並無什麼不一樣,可是若是你正在學習數組到指針的退化 ,你仍是應該同時瞭解一下函數到指針退化比較好。

 

因此,到這裏你應該知道了模板類型推導的規則,在最開始的時候我就說他們是如此的簡單明瞭,事實上,對於大多數規則而言,也確實是這樣的,惟一可能會激起點水花的是在使用萬能引用(universal references)時,左值有着特殊的待遇,甚至數組和函數到指針的退化規則會讓水變得渾濁,有時,你可能只是簡單的抓住你的編譯器,」告訴我,你推導出的類型是什麼「,這時候,你能夠看看條款4,由於條款4就是講述如何勸誘你的編譯器這麼作的。

 

請記住:

  • 當模板的參數是一個指針或是一個引用,但不是一個萬能引用(universal reference)時,實例化的表達式是不是一個引用將被忽略。
  • 當模板的參數是萬能引用(universal reference)時,左值的實參產生左值的引用,右值的實參產生右值的引用。
  • 模板的參數是按值傳遞的時候,實例化的表達式的引用性和常量性將被忽略。
  • 在類型推導期間,數組和函數將退化爲指針類型,除非他們是被實例化爲相應的引用。
相關文章
相關標籤/搜索