現代C++之理解模板類型推斷(template type deduction)

理解模板類型推斷(template type deduction)

咱們每每不能理解一個複雜的系統是如何運做的,可是卻知道這個系統可以作什麼。C++的模板類型推斷即是如此,把參數傳遞到模板函數每每能讓程序員獲得滿意的結果,可是卻不可以比較清晰的描述其中的推斷過程。模板類型推斷是現代C++中被普遍使用的關鍵字auto的基礎。當在auto上下文中使用模板類型推斷的時候,它不會像應用在模板中那麼直觀,因此理解模板類型推斷是如何在auto中運做的就很重要了。html

下面將詳細討論。看下面的僞代碼:程序員

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

經過下面的代碼調用:express

f(expr); //call  f with some expression

在編譯過程當中編譯器會使用expr推斷兩種類型:一個T的類型,一個是ParamType。而這兩種類型每每是不同的,由於ParamType一般會包含修飾符,好比const或者引用。若是一個模板被聲明爲下面這個樣子:數組

template<typename T>
void f(const T& param);//ParamType is const T&

經過以下代碼調用:app

int x = 0;
f(x); //call f with an int

T會被推斷成int,可是 ParamType會被推斷成const int&。函數

咱們很天然的會認爲T的推斷類型和傳遞到函數的參數類型是相同的,上面的例子就是這樣的,參數x的類型爲int,T也被推斷成了int類型。可是每每狀況不是這樣子的。對T的類型推斷不只僅依賴參數expr的類型,也依賴ParamType的形式。指針

有三種狀況:code

  • ParamType是指針或者引用類型,但不是universal reference(這個類型在之後的篇章中會講到,如今只須要明白,這種類型不一樣於左值引用和右值引用便可。)
  • ParamType是universal reference。
  • ParamType即非指針也非引用。

下面將分別進行舉例,每一個例子都從下面的模板聲明和函數調用僞代碼演變而來:htm

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

ParamType是指針或者引用類型

這種狀況下的類型推斷會是下面這個樣子:對象

  • 若是expr的類型是引用,忽略引用部分。
  • 而後將expr的類型同ParamType進行模式匹配來最終決定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爲指向const int的引用

對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會變爲類型的一部分。

第三個例子中,rx的類型是引用類型,T卻被推斷爲非引用類型。由於類型推斷過程當中rx的引用類型會被忽略。

上面的例子只是說明了左值引用參數,對於右值引用參數一樣試用

若是咱們將函數f的參數類型改爲cont T&,實參cx和rx的const屬性確定不會變,可是如今咱們將參數聲明成爲指向const的引用了,所以沒有必要將const推斷成爲T的一部分:

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

聲明的變量不變:

int x = 27; //不變
const int cx = x;//不變
const int& rx = x;//不變

對param和T的推斷以下:

f(x); //T被推斷爲int,param的類型被推斷爲const int &
f(cx);//T被推斷爲int,param的類型被推斷爲const int &
f(rx);//T被推斷爲int(引用一樣被忽略) ,param的類型被推斷爲const int &

若是param是指針或者指向const的指針,本質上同引用的推斷過程是相同的。

指針和引用做爲模板參數在推斷過程當中的結果是顯而易見的,下面的例子就隱晦一些了。

ParamType是一個Universal Reference

這種類型的參數在聲明時形式上同右值引用相似(若是一個函數模板的類型參數爲T,將其聲明爲Universal Reference寫成TT&&),可是傳遞進來的實參若是爲左值,結果同右值引用就不太同樣了(之後會講到)。

Universal Reference的模板類型推斷將會是下面這個樣子:

  • 若是expr是一個左值,T和ParamType都會被推斷成左值引用。有點難以想象,首先,這是模板類型推斷中惟一將T推斷爲引用的狀況;其次,雖然ParamType的聲明使用右值引用語法,但它最終卻被推斷成左值引用。
  • 若是expr是一個右值,參考上一節(ParamType是指針或者引用類型)。

舉個例子:

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

int x = 27; //不變
const int cx = x;//不變
const int& rx = x;//不變

對param和T的推斷以下:

f(x); //x爲左值,所以T爲int&,ParamType爲 int&
f(cx);//cx爲左值,所以T爲const int&,ParamType也爲const int&
f(rx);//rx爲左值,所以T爲const int&,ParamType也爲const int&
f(27);//27爲右值,T爲int ,ParamType爲int&&

這裏的關鍵點是,模板參數爲Universal Reference類型的時候,對於左值和右值的推斷狀況是不同的。這種狀況在模板參數爲非Universal Reference類型的時候是不會發生的。

ParamType既不是指針也不是引用

這種狀況也就是所謂的按值傳遞:

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

傳遞到函數f中的實參值會是原來對象的一份拷貝。這決定了如何從expr中推斷T:

  • 同狀況一相似,若是expr的類型是引用,忽略引用部分。
  • 若是expr是const的,一樣將其忽略。若是是volatile的,一樣忽略。

看例子:

int x = 27; //不變
const int cx = x;//不變
const int& rx = x;//不變

對param和T的推斷以下:

f(x); // T爲int ParamType爲 int
f(cx);//同上
f(rx);//同上

能夠看到即便cx和rx爲const,param也不是const的。由於param只是cx和rx的一份拷貝,因此不論param的類型如何都不會對原值形成影響。不能修改expr並不意味着不能修改expr的拷貝。

注意只有param是by-value的時候,const或者volatile纔會被忽略。咱們在前面的例子中說明了,若是參數類型爲指向const的引用或者指針,類型推斷過程當中expr的const屬性會被保留。可是看一下下面的狀況,若是expr爲指向const對象的const指針,而param的類型爲by-value,結果會是什麼樣子的呢:

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

const char * const ptr = "Fun with pointers";
f(ptr);

咱們先回憶一下const指針,星號左邊的const(離指針最近)表示指針是const的,不能修改指針的指向,星號右邊的const表示指針指向的字符串是const的,不能修改字符串的內容。當ptr傳遞給f的時候,指針自己是按值傳遞的。由於在by-value參數的類型推斷中const屬性會被忽略,所以指針的const也就是星號右邊的const會被忽略,最後推斷出來的參數類型爲const char * ptr,也就是能夠修改指針指向,不能修改指針所指內容。

數組參數

上面的三種狀況涵蓋了模板類型推斷的大部分狀況,可是有另一種狀況不得不說,就是數組。雖然數組和指針有時候看上去是能夠互換的,形成這種幻覺的一個主要緣由是在許多狀況下,數組能夠退化爲指向第一個數組元素的指針,正是這種退化下面的代碼才能編譯經過:

const char name[]="HarlanC";//name的類型爲const char[8]
const char*ptrToName = name;//數組退化成指針

雖然指針和數組的類型不一樣,但因爲數組退化爲指針的規則,上邊的代碼可以編譯經過。

若是將數組傳遞給帶有by-value參數的模板,會發生什麼呢?

template <typename T>
void f(T param);//按值傳遞
f(name);

將數組做爲函數參數的語法是合法的。

void myFunc(int param[]);

可是這裏的數組參數會被當作指針參數來處理,也就是說下面的聲明和上面的聲明是等價的:

void myFunc(int* param); // same function as above

由於數組參數會被當作指針參數來處理,因此將一個數組傳遞給按值傳遞的模板函數會被推斷爲一個指針類型。當調用模板函數f的時候,類型參數T會被推斷成const char*:

f(name); // name is array, but T deduced as const char*

雖然函數不能聲明一個真正的數組參數(即便這麼聲明也會被當作指針來處理),可是可以將參數聲明爲指向數組的引用。咱們將模板函數作以下修改:

template <typename T>
void f(T& param);//按引用傳遞

傳遞一個數組實參:

f(name);

這時候會將T推斷成一個真正的數組類型。這個類型同時包含了數組的大小,在上面的例子中,T會被推斷成const char [8],而f的參數類型爲const char (&)[8]。

使用這種聲明有一個妙用。咱們能夠建立一個模板來推斷出數組中包含的元素數量

//在編譯期返回數組大小 ,
//注意下面的函數參數是沒有名字的
//由於咱們只關心數組的元素數量
template<typename T, std::size_t N> 
constexpr std::size_t arraySize(T (&)[N]) noexcept 
{ 
    return N; 
}

將函數返回值聲明成constexpr類型的意味着這個值在編譯期就可以獲得。這樣咱們能夠在編譯期獲取一個數組的大小,而後聲明另一個相同大小的數組:

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
int mappedVals[arraySize(keyVals)];

使用std::array更可以體現你是一個現代C++程序員:

std::array<int, arraySize(keyVals)> mappedVals;

函數參數

數組不是可以退化成指針的惟一類型。函數類型也可以退化爲指針,咱們所討論的關於數組的類型推斷過程一樣適用於函數:

void someFunc(int, double); // someFunc是一個函數,類型爲void(int, double)

template<typename T>
void f1(T param); //passed by value
template<typename T>
void f2(T& param); // passed by ref
f1(someFunc); // param 被推斷爲 ptr-to-func void (*)(int, double)
f2(someFunc); // param 被推斷爲ref-to-func void (&)(int, double)

要點總結

  • 模板類型推斷會把引用當作非引用來處理,也就是說會把參數的引用屬性忽略掉。
  • 當模板參數類型爲universal reference 時,進行類型推斷會對左值入參作特殊處理。
  • 當模板類型參數爲by-value時,const或者volatile會被當作非const或者非volatile處理。
  • 當模板類型參數爲by-value時,入參爲函數或者數組時會退化爲指針。
相關文章
相關標籤/搜索