程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 《Effective Modern C++》翻譯--條款1: 理解模板類型推導

《Effective Modern C++》翻譯--條款1: 理解模板類型推導

編輯:C++入門知識

《Effective Modern C++》翻譯--條款1: 理解模板類型推導


北京2016年1月9日13:47:17 開始第一章的翻譯。
第一章名為 類型推斷
分為四個條款:
1理解模板類型推導
2理解auto自動類型推導
3理解decltype操作符
4如何對待推導的類型

第一章 類型推導

C++98有一套單一的類型推導的規則用來推導函數模板。C++11輕微的修改了這些規則並且增加了兩個推導規則,一個用於auto,一個用於decltype。接著C++14擴展了auto和decltype可以使用的語境。類型推導的普遍應用將程序員從必須拼寫那些顯然多余的類型中解放了出來,它使得C++開發的軟件更有彈性,因為在某處改變一個類型會自動的通過類型推導傳播到其他的地方。它使C ++軟件適應性更強,因為改變一個類型在源代碼中一個點自動通過類型推演到其他地方傳播。但是,它可以使代碼更難推理,因為編譯器推斷該類型可能不像我們想象的那麼明顯。

想要在現代C++中進行有效率的編程,你必須對類型推導操作有一個扎實的了解。因為有太多的情形你會用到它:在函數模板的調用中;在auto出現的大多數場景中;在decltype表達式中;在C++14中在神秘的decltype(auto)構造被應用的時候。

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

條款1: 理解模板類型推導
有人說,模仿是最真誠的奉承形式,但幸福的無知可以是同樣由衷的贊譽。當使用者使用一個復雜的系統,忽視了它的系統是如何設計的,是如何工作的,然而對它的所完成的事情你依舊會感到很高興,通過這種方式,C++中模板的類型推導成為了一個巨大的成功,數百萬的程序員向模板函數中傳遞參數,並獲得完全令人滿意的答案,盡管很多程序員被緊緊逼著的去付出比對這些函數是如何被推導的一個朦胧的描述要更多。

如果上面提到的人群中包括你,我有一個好消息和一個壞消息。好消息是對於auto聲明的變量的類型推導規則和模板在本質上是一樣的,所以當涉及到auto的時候,你會感到很熟悉(見條款2)。壞消息是當模板類型推導的規則應用到auto的時候,你很可能對發生的事情感到驚訝,如果你想要使用auto(相比精確的類型聲明,你當然更應該使用auto,見條款5),你需要對模板類型推導規則有一個合理正確的認識,他們通常是直截了當的,所以這不會照成太大的挑戰,和在C++98裡工作的方式是一樣的,你很可能不需要對此有過多的思考。

如果你願意忽略少量的偽代碼,我們可以直接對下面的模板函數的代碼進行思考:

temppate
void f(ParamType param);

可以這樣調用:

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

在編譯時期,編譯器根據expr來推導兩個類型:一個是T的,一個是ParamType的。這兩個類型經常是不同的,因為ParamType經常包括例如const或者是引用的限定的一些修飾符。例如,如果模板像下面這樣聲明:

temppate
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與傳遞給函數的參數的類型相同,就像T是expr類型一樣。在上面的例子中,就是這樣,x是int類型,而T被推導為int類型。但是情況並不總是這樣。T被推導的類型不僅僅取決於expr,也與ParamType類型有關。有三種情況:

?ParamType是一個指針或是引用類型,但不是一個universal reference(universal reference在條款26中進行闡述。目前,你只要知道universal reference的存在就可以了)。

?ParamType是一個universal reference。

?ParamType既不是指針也不是引用。

因此,我們會有三種類型推導的情景,每一個調用都會以我們通用的模板形式為基礎:

template
void f(ParamType param);

f(expr);   //deduce T and ParamType from expr

第一種情況:ParamType是一個指針或是引用類型,但不是一個universal reference

最簡單的情況就是當ParamType是一個指針或是引用類型,但不是universal reference的時候。在這樣的情況下,類型推導是這樣工作的:

?如果expr是引用類型,則忽略引用部分。

?通過模式匹配expr的類型來決定ParamType的類型從而決定T的類型

例如,如果我們的模板是這樣的:

template
void f(T& param);  //param是一個引用

並且,我們如下聲明變量:

int x = 27;        //x is an int
const int cx = x;  //cx is a const int
const int& rx = x; //rx is a read-only view of x

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

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&

在第二個和第三個函數調用中,注意到因為cx和rx被指派為const了,T被推導為const int,因此產生的參數類型是const int&。這對調用者來說是十分重要的。當他們向一個引用類型的參數傳遞一個const對象時,他們期待這個對象依舊是無法被修改的,比如,這個參數的類型被推導為一個指向const的引用。這就是為什麼向帶有一個T&參數的模板傳遞一個const對象是安全的,對象的常量性成為了推導出的類型T的一部分。

在第三個例子中,注意到盡管rs是一個引用類型,T被推導為一個非引用類型,這是因為rs的引用性(reference-ness)在推導的過程中被忽略了,如果不是這樣的話(例如,T被推導為const int&),param的類型將會是const int&&,一個引用的引用,引用的引用在C++裡是不允許的,避免他們的唯一方法在類型推導時忽略表達式的引用性。

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

如果我們把f的參數類型由T&改成const T&,事情會發送一點小小的改變,但不會太讓人驚訝。cx和rx的常量性依舊滿足,但是因為我們現在假定了param是一個常量的引用,const不在需要被推導為T的一部分了:

template
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的引用性被忽略了。

如果param是一個指針類型(或者是指向常量的指針)而不是引用類型,還是按照同樣的方式進行類型推導。

template
void f(T* param); //param is now a pointer

int x = 27;             //as before
const int *px = &x;     //px is a ptr to a read-only view of x

f(&x);                   //T is int, param's type is int*

f(px);                   //T is const int, param's type is const int*

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

第二種情況:ParamType是一個universal reference

當涉及到universal reference作為模板的參數的時候(例如 T&&參數),事情變得不是那麼清楚了,因為規則對於左值參數有著特殊的對待。完整的內容將在條款26中講述,但這裡有一個概要的版本:

?如果expr是一個左值,T和ParamType都被推導為一個左值的引用

?如果expr是一個右值,使用通常情況下的類型推導規則

例如:

template
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 therefore int&&

條款26詳細解釋了為什麼,但現在重要的是類型推導對於模板的參數是univsersal references和參數是左值或右值時規則是不同的。當使用univsersal references的時候,類型推導規則會區別左值和右值,而這從來不會發生在nivsersal references的引用上。

第三種情況:ParamType既不是指針也不是引用

當ParamType既不是指針也不是引用的時候,我們按照按值傳遞進行處理:

template
void f(T param);     //param is now passed by value

這意味著無聊傳遞的是什麼,param都將成為它的一個拷貝–完全一個新的對象。事實上,param是一個全新的對象控制導出了T從expr中推導的規則:

?和之前一樣,如果expr的類型為引用,那麼將忽略引用部分。

?如果在expr的引用性被忽略之後,expr帶有const修飾,忽略const,如果帶有volatile修飾,同樣忽略(volatile對象是不尋常的對象,他們通常僅被用來實現設備驅動程序,更多的細節,可以參照條款42)。

所以:

int x = 27;             //as before
const int cx = x;       //as before
const int& rx = x;      //as before

f(x);                   //T and param are both int

f(cx);                  //T and param are again both int

f(rx);                  //T and param are still both int

注意,盡管cx和rx代表的是常量,但param不是常量。這很有意義。因為parm是和cx,rx完全獨立的對象,它是cx和rx的一個拷貝。事實上cx和rx不能被修改和param是否能被修改沒有任何的關系,這就是為什麼expr的常量性在推導param類型的時候被忽略了;因為expr不能被修改並不意味著它的拷貝也不能被修改。

注意到const僅僅在按值傳遞的參數中被忽略掉是很重要的。正如我們看到的那樣,對於指向常量的引用和指針來說,expr的常量性在類型推導的時候是被保留的。但是考慮下面的情況,expr是一個指向const對象的常量指針,並且expr按值傳遞給一個參數:

template
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 type 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's type is const char[13]

const char* ptrToName = name;       //array decays to pointer

這裡,常量類型的字符串指針ptrToName被初始化為name,而name是一個常量數組,也就相當於是具有十三個常量字符元素的數組。這些類型(const char*和const char[13])是不一樣的,但是由於數組到指針的退化,這樣的代碼是可以編譯通過的。

但是,如果模板的參數是一個按值傳遞的數組類型呢?那又會發生什麼呢?

template
void f(T param);       //template with by-value parameter

f(name);               //what types are deduced for T and param?

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

void myFunc(int param[]);

但是,此時的數組聲明被看著與指針聲明一樣,這就意味著函數myFunc可以這樣等價聲明為:

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

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

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

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

但是現在來了一個曲線球,盡管函數不能聲明一個真正意義上的數組類型的參數,但是他們可以聲明一個指向數組的引用,所以如果我們把模板f改成按引用傳遞參數:

template
void f(T& param);     //template with by-reference paremeter

然後 ,我們給他傳遞一個數組:

f(name);              //pass array to f

T的類型被推導為數組的類型。這個類型包括了數組的大小,所以在上面這個例子中,T被推導為const char[13],f的參數的類型(對數組的一個引用)是const char(&)[13]。對的,這個語法看起來是有害的,但是從好的的方面看,知道這些將會獎勵你那些別人得不到的罕見的分數(知道這些對你有好處)。

有趣的是,聲明一個指向數組的引用能夠讓我們創建一個模板來返回數組的長度,也就是數組具有的元素個數:

template       //return size of
constexpr std::size_t arraySize(T (&)[N]) //an array as a
{                                         //compile-time
   return N;                              //constant
}

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

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

當然,作為一個現代C++的開發者,你應該更習慣的使用std::array而不是內置的數組:

std::array mappedVals; // mappedVals'
                                                // size is 7

函數作為參數

在C++中,數組不是唯一能夠退化成指針的實體。函數類型同樣可以退化成函數指針,並且我們討論的任何一個關於類型推導的規則和對數組相關的事情對於函數的類型推導也適用,函數類型會退化為函數的指針。因此:

void someFunc(int, double); // someFunc is a function;
                            // type is void(int, double)
template
void f1(T param);           // in f1, param passed by value

template
void f2(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 as ref-to-func;
                            // type is void(&)(int, double)

這和數組實際上並沒有什麼不同。但是如果你想學習數組到指針的退化 ,你還是應該同時了解一下函數到指針退化比較好。

所以,到這裡你應該知道了模板類型推導的規則,在最開始的時候我就說他們是如此的簡單明了。事實上,對於大多數規則而言,也確實是這樣的,唯一可能會激起點水花的是在使用universal references時,左值有著特殊的待遇,甚至數組和函數到指針的退化規則會讓水變得渾濁。有時,你可能只是簡單的抓住你的編譯器,”告訴我,你推導出的類型是什麼“。當這種情況發生時,轉向條款4,因為它是專門用於哄騙編譯器將這樣做。

請記住:

?當模板的參數是指針或是引用,但不是universal reference時,初始化的表達式是否是一個引用將被忽略。

?當模板的參數是universal reference時,左值的實參產生左值的引用,右值的實參產生右值的引用。

?模板的參數是按值傳遞的時候,實例化的表達式的引用性和常量性將被忽略。

?在類型推導期間,數組和函數將退化為指針類型,除非他們是被初始化化為引用。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved