條款2: 理解auto自動類型推導
如果你已經讀過條款1關於模板類型推導的內容,那麼你幾乎已經知道了關於auto類型推導的全部。至於為什麼auto類型推導就是模板類型推導只有一個地方感到好奇。那是什麼呢?即模板類型推導包括了模板、函數和參數,而auto類型推斷不用與這些打交道。
這當然是真的,但是沒關系。模板類型推導和auto自動類型推導是直接匹配的。從字面上看,就是從一個算法轉換到另一個算法而已。
在條款1中,闡述模板類型推導采用的是常規的函數模板:
template
void f(ParamType param);
並且是常規方法進行調用:
f(expr); //call f with some expression
在調用f的過程中,編譯器通過expr推導出T和ParamType的類型。
當使用auto關鍵字聲明一個變量時,auto關鍵字就扮演者上述模板中T的角色,並且類型說明符與ParamType扮演者同樣的角色。比語言描述更加清晰的是展示他們,所以請看這樣的例子:
auto x = 27;
這樣,x的類型說明符與自己一樣。另一方面,這樣聲明:
const auto cx = x;
這裡的類型說明符是const auto。而這裡:
const auto& rx = x;
此時類型說明符是const auto&。為了推斷上面這些例子中的x,cx和rx變量,編譯器起到的作用就是為每個聲明提供一個模板,並且使用相應的初始化表達式來調用這些模板:
template //conceptual template for deducing x's type
void func_for_x(T param);
func_for_x(27); //conceptual call: param's deduced type is x's type
template //conceptual template for deducing cx's type
void func_for_cx(const T param);
func_for_cx(x); //conceptual call: param's deduced type is cx's type
template //conceptual template for deducing rx's type
void func_for_rx(const T& param);
func_for_rx(x); //conceptual call: param's deduced type is rx's type
正如我之前所說,auto類型推導與模板類型推導是一樣的。
條款1中根據ParamType和在常規模板中param的類型說明符,把模板類型推導分為三種情況。在通過auto進行變量類型推導時,類型說明符替代了ParamType,但也是分為三個情況:
?第一種情況:類型說明符是一個指針或是引用,但不是universal reference。
?第二種情況:類型說明符是一個universal reference。
?第三種情況:類型說明符既不是指針也不是引用。
我們分別看看第一和第三種情況的例子:
auto x = 27; //case 3 (x is neither ptr nor reference)
const auto cx = x; //case 3 (cx isn't either)
const auto& rx = x; //case 1 (rx is non-universal ref.)
第二種情況正如你期待的那樣:
auto&& uref1 = x; //x is int and lvalue, so uref1's type is int&
auto&& uref2 = cx; //cx is const int and lvalue, so uref2's type is const int&
auto&& uref3 = 27; //27 is int and rvalue, so uref3's type is int&&
條款1中總結了對於非引用類型的說明符,數組和函數名如何退化為指針。這當然同樣適用於auto類型推導:
const char name[] = "R. N. Briggs"; //name's type is const char[13]
auto arr1 = name; //arr1's type is const char*
auto& arr2 = name; //arr2's type is const char (&)[13]
void someFunc(int, double); //someFunc is a function; type is void(int, double)
auto func1 = someFunc; //func1's type is void(*)(int, double)
auto& func2 = someFunc; //func2's type is void(&)(int, double)
正如您所見,auto類型推導真的和模板類型推導是一樣的。就好比一枚硬幣的兩個面相同。
您肯定期待二者的不同。讓我從觀察聲明一個變量並初始化為27開始,在C++98中,給了你兩種語法選擇:
int x1 = 27;
int x2(27);
C++11中還增加了這個:
int x3 = {27};
int x4{27};
總之,四種語法都得到了一個結果,就是把變量初始化為27。
但是正如條款5解釋的那樣,使用auto代替固定的類型來聲明變量有很多的優勢,所以使用auto關鍵字代替上面程序中的int類型。簡單的文本替換我們就得到這樣的代碼:
auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{27};
上面的這些都可以通過編譯,但是與之前的相比,代表的含義已經不同了。上面四個表達式中前兩個實際上是聲明一個值為27int類型的變量。而後兩個,是聲明一個類型為std::initializer_list
,並且具有一個值為27的元素!
auto x1 = 27; //type is int, value is 27
auto x2(27); //同上
auto x3 = {27}; //type is std::initializer_list, value is {27}
auto x4{27}; //同上
這是因為auto類型推導有特俗的規則。當為auto聲明的類型進行初始化使用封閉的大括號時,則推導的類型為std::initializer_list。如果這樣的類型不能被推導,這樣的編碼是被編譯器拒絕的:
auto x5 = {1, 2, 3.0}; //error! can't deduce T for std::initializer_list
正如上面注釋中提示的那樣,這樣的情況下類型推導會失敗,但更重要的是認清此時采用了兩種類型推導。一個是通過auto完成對x5的類型推導。由於x5是使用大括號進行的初始化,所以x5必須被推導為std::initializer_list類型。但是std::initializer_list是一個模板。對於std::initializer_list
的實例化需要推導T的類型。這樣的類型推導發生第二種類型推導的管轄范圍:模板類型推導。在這個例子中,類型推導失敗,因為初始化值不具有單一類型。
對待使用大括號進行初始化,auto類型推導和模板類型推導是有區別的。當一個auto變量被大括號進行初始化時,推導的類型為std::initializer_list實例化的類型。如果一個模板面臨著對於大括號初始化進行類型推導,這樣的代碼是被拒絕的。(條款32將解釋完美的轉發)
你會懷疑為什麼對於大括號進行初始化,auto類型推導會有特殊的規則呢,而模板類型推導則沒有。我自己對此也表示懷疑。不幸的是,我找不到令人信服的解釋。但是規則就是規則,這也就意味著對於auto推導用大括號初始化的變量時你必須銘記於心,推導類型的總是std::initializer_list。這是特別重要的是要牢記這一點,如果你面對在大括號包圍初始化值作為理所當然的初始化的理念。在C++11編程中,其中最典型的錯誤是你想聲明一個其他類型的變量,卻聲明一個std :: initializer_list變量。重申一下:
auto x1 = 27; //x1 and x2 are ints
auto x2(27);
auto x3 = {27}; //x3 and x4 are std::initializer_lists
auto x4{27};
這個陷阱使得一些開發者只有迫不得已的時候才使用大括號初始化變量。(我們在條款7中再討論。)
對於C++11來說,這裡是全部的內容,但是對於C++14,故事還在繼續。C++14允許使用auto去推導函數的返回值(參見條款3),並且C++14中的lambda表達式在參數聲明時可以使用auto類型推導。但是,這裡使用的auto推導采用的是模板推導的規則,而不是auto類型推導的規則。這就意味著,使用大括號初始化將造成類型推導失敗。所以使用auto作為返回類型的函數返回一個使用大括號初始化的變量編譯不會通過:
auto createInitList()
{
return {1, 2, 3}; //error: can't deduce type for {1, 2, 3}
}
同樣的道理也適用於C++14中lambda表達式使用auto作為參數(因此產生一個通用lambda表達式):
std::vector v;
....
auto resetV = [&v](const auto& newValue) { v = newValue;};//C++14 only
....
resetV({1, 2, 3}); //error! can't deduce type for {1, 2, 3}
最終的結果是,如果不使用大括號進行初始化,auto類型推導是和模板類型推導一致的。僅在這種情況下(使用大括號進行初始化),auto推導為一個std:: initializer_list,但模板類型推導會失敗。
請記住:
?auto類型對象通常與模板類型推導是相同的。
?唯一的例外是:使用auto和大括號進行初始化時,自動推導為std::initializer_lists。
?對於使用括號進行初始化,模板類型推導會失敗。