條款2 明白auto類型推導
如果你已經讀完了條款1中有關模板類型推導的內容,那么你幾乎已經知道了所有關于auto類型推導的事情,因為除了一個古怪的例外,auto的類型推導規則和模板的類型推導規則是一樣的,但是為什么會這樣呢?模板的類型推導涉及了模板,函數和參數,但是auto的類型推導卻沒有涉及其中的任何一個。
?
這確實是對的,但這無關緊要,在auto類型推導和template之間存在一個直接的映射,可以逐字逐句的將一個轉化為另外一個。
?
在條款1中,模板類型推導是以下面的模板形式進行舉例講解的:
template<typename T>
void f(ParamType param);
函數調用是這樣
f(expr); //用一些表達式調用函數f
在f的函數調用中,編譯器使用expr來推導T和ParamType的類型。
?
當一個變量用auto進行聲明的時候,auto扮演了模板中的T的角色,變量的類型說明符(The type specifier)相當于ParamType,這個用一個例子來解釋會更容易一些,考慮下面的例子:
auto x=27;
這里x的類型說明符就是auto本身,在另一方面,在下面這個聲明中:
const auto cx=x;
類型說明符是const auto。
const auto& rx=x;
類型說明符是const auto&,在上面的例子中,為了推導x,cx,rx的類型,編譯器會假裝每一個聲明是一個模板,并且用相應的初始化表達式來調用(compilers act as if there were a template for each declaration as well as a call to that template with the corresponding initializing expression:)
template<typename T> // 產生概念上的模板來 void func_for_x(T param); // 推導x的類型 func_for_x(27); // 概念上的函數調用,參數 // 推導出的類型就是x的類型 template<typename T> // 產生概念上的模板來 void func_for_cx(const T param); // 推導cx的類型 func_for_cx(x); // 概念上的函數調用,參數 // 推導出的類型就是cx的類型 template<typename T> // 產生概念上的模板來 void func_for_rx(const T& param); // 推導cx的類型 func_for_rx(x); // 概念上的函數調用,參數 // 推導出的類型就是rx的類型
就像我說的那樣,auto的類型推導和模板的類型推導是一樣的。
?
條款1把模板的類型推導按照ParamType的類型,分成了3種情況,同樣,在auto聲明的變量中,變量的類型說明符(The type specifier)相當于ParamType,所以auto類型推導也有3種情況:
- 情況1:類型說明符是一個指針或是一個引用,但不是一個萬能引用(universal reference)
- 情況2:類型說明符是一個萬能引用(universal reference)
- 情況3:類型說明符既不是指針也不是引用
?
我們在上面已經舉過了情況1和情況3的例子
auto x = 27; //條款3(x既不是指針也不是引用) const auto cx = x; //條款3(cx既不是指針也不是引用) const auto& rx = x; //條款1(rx不是一個萬能引用)
情況2也像你想的那樣
auto&& uref1 = x; // x的類型是int并且是一個左值 // 所以uref1的類型是 auto&& uref2 = cx; // cx的類型是const int并且是一個左值 // 所以uref2的類型是const int& auto&& uref3 = 27; // 27的類型是int并且是一個右值 // 所以uref3的類型是int&&
條款1同樣也討論了數組和函數名在非引用類型的類型說明符下,會退化為指針類型,這當然同樣適用于auto的類型推導
const char name[] = "R. N. Briggs"; //name的類型是const char[13] name's type is const char[13] auto arr1 = name; //arr1的類型是const char* auto& arr2 = name; //arr2的類型是 // const char (&)[13] void someFunc(int, double); //someFunc是一個函數; //類型是void(int, double) auto func1 = someFunc; // func1的類型是 // void (*)(int, double) auto& func2 = someFunc; // func2的類型是 // void (&)(int, double)
就像你看到的那樣,auto類型推導其實和模板類型推導是一樣的,他們就相當于硬幣的正反兩個面。
?
但是在一點上,他們是不同的,如果你想把一個聲明一個變量,它的初始值是27,C++98中,你可以使用下面的兩種語法
int x1 = 27; int x2(27);
在C++11中,提供對統一的集合初始化(uniform initialization)的支持,增加下面是聲明方式。
int x3 = {27}; int x4{27};
總而言之,上面的4種聲明方式的結果是一樣的,聲明了一個變量,它的初始值是27。
?
但是就像條款5解釋的那樣,使用auto聲明變量要比使用確定的類型聲明更有優勢,所以將上面代碼變量聲明中的int替換成auto會是非常好的,直接的文本上的替換產生了下面的代碼:
auto x1 = 27; auto x2(27); auto x3 = {27}; auto x4{27};
這些聲明都能夠通過編譯,但他們并非全和替代前有著同樣的意義,前兩個的確聲明了一個int類型的變量,初始值為27;然而,后兩個聲明了一個std::initializer_list<int>類型的變量,它包括一個元素,初始值是27;
auto x1 = 27; // 類型是int,初始值是27 auto x2(27); // 同上 auto x3 = {27}; //類型是std::initializer_list<int>//初始值是27 auto x4{27}; //同上
?
這是由于auto類型推導的一個特殊的規則,當變量使用大括號的初始化式(braced initializer)初始化的時候,被推導出的類型是std::initializer_list,如果這個類型不能被推導出來(比如,大括號的初始化式中的元素有著不同的類型),代碼將不能通過。
auto x5 = {1, 2, 3.0}; // 錯誤!無法推導出std::initializer_list<T>中T的類型
就像注釋里指出的的那樣,類型推導在這種情況下失敗了,但是,重要的是認識到這里其實發生了兩種形式的類型推導,一種來源于auto的使用,x5的類型需要被推導出來,另外因為auto是用大括號的初始化式初始化的,x5的類型必須被推導為std::initializer_list,但是std::initializer_list是一個模板,所以實例化模板std::initizalizer_list<T>意味著T的類型必須被推導出來,在上面的例子中,模板的類型推導失敗了,因為大括號里變量類型不是一致的。
?
對待大括號的初始化式(braced initializer)的不同是auto類型推導和模板類型推導的唯一區別,當auto變量用一個大括號的初始化式(braced initializer)初始化的時候,推導出的類型是實例化后的std::initializer_list模板的類型,而模板類型推導面對大括號的初始化式(braced initializer)時,代碼將不會通過(這是由于完美轉發perfect forwarding的結果,將在條款32中進行講解)
?
你可能會猜想為什么auto類型推導對于大括號的初始化式(braced initializer)有著特殊的規則,而模板類型推導確沒有,我也想知道,不幸的是,我沒有找到一個吸引人的解釋,但是規則就是規則,這意味著,你必須記住如果你用auto聲明一個變量,并且用大括號的初始化式進行初始化的時候,推導出的類型總是std::initializer_list,如果你想更深入的使用統一的集合初始化時,你就更要牢記這一點,(It’s especially important to bear this in mind if you embrace the philosophy of uniform initialization of enclosing initializing values in braces as a matter of course.)C++11的一個最經典的錯誤就是程序員意外的聲明了一個std::initializer_list類型的變量,但他們的本意卻是想聲明一個其他類型的變量。讓我再重申一下:
auto x1 = 27; // x1和 x2都是int類型 auto x2(27); auto x3 = {27}; // x3和x4是 auto x4{27}; // std::initializer_list<int>類型
?
陷阱的主要原因是一些程序員只有當必要的時候,才使用大括號的初始化式進行初始化)(This pitfall is one of the reasons some developers put braces around their initializers only when they have to. (什么時候你必須時候將在條款7中討論)
?
對于C++11,這已經是一個完整的故事了,但是對于C++14,故事還沒有結束,C++14允許auto來指出一個函數的返回類型需要被推導出來(見條款3),C++14的lambda表達式可能需要在參數的聲明時使用auto,不管怎樣,這些auto的使用,采用的是模板類型推導的規則,而不是auto類型推導規則,這意味著,大括號的初始化式會造成類型推導的失敗,所以一個帶有auto返回類型的函數如果返回一個大括號的初始化式將不會通過編譯。
auto createInitList() { return { 1, 2, 3 }; // 錯誤: 無法推導出 } // { 1, 2, 3 }的類型
同樣,規則也適用于當auto用于C++14的lambda(產生一個通用的lambda(generic lambda))的參數類型說明符時,
std::vector v;auto resetV = [&v](const auto& newValue) { v = newValue; }; //只在C++14下允許 … resetV( { 1, 2, 3 } ); //錯誤! 無法推導出 //{ 1, 2, 3 }的類型
?
最終結果是auto類型推導和模板類型推導是完全相同的,除非(1)一個變量被聲明了,(2)它的初始化是用大括號的初始化式進行初始化的(its initializer is inside braces),只有這種情況下,auto下被推導為std::initializer_list,而模板會失敗。
?
請記住:
- auto的類型推導通常和模板類型推導完全相同。
- 唯一的例外是,當變量用auto聲明,并且使用大括號的初始化式初始化時,auto被推導為std::initializer_list。
- 模板類型推導在面對大括號的初始化式(braced initializer)初始化時會失敗。