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

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

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

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

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

通過下面的代碼調用:

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

在編譯過程中編譯器會使用expr推斷兩種類型:一個T的類型,一個是ParamType。而這兩種類型往往是不一樣的,因爲ParamType通常會包含修飾符,比如const或者引用。如果一個模板被聲明爲下面這個樣子:

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

通過如下代碼調用:

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

T會被推斷成int,但是 ParamType會被推斷成const int&。

我們很自然的會認爲T的推斷類型和傳遞到函數的參數類型是相同的,上面的例子就是這樣的,參數x的類型爲int,T也被推斷成了int類型。但是往往情況不是這樣子的。對T的類型推斷不僅僅依賴參數expr的類型,也依賴ParamType的形式。

有三種情況:

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

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

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時,入參爲函數或者數組時會退化爲指針。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章