類型推導(dǎo)規(guī)則
在大多數(shù)情況下,模板與auto的類型推導(dǎo)規(guī)則一致,且規(guī)則很簡(jiǎn)單廊遍。
情況1. 沒有加任何修飾
// 模板函數(shù):
template <typename T>
f(T t) {...}
// auto聲明:
auto x = ...;
這種情況下,參數(shù)是按值傳遞贩挣,形參t或者變量x都是一個(gè)副本喉前,那么就需要去掉引用没酣,且副本本身沒必要加cv限定符(const、volatile卵迂,下文都不考慮volatile裕便,只說const),也需要去掉见咒。
const int& a = ...;
f(a); // T 推導(dǎo)為 int
auto x = a; // x 的類型為 int
但需要注意闪金,只能去掉變量本身的const,對(duì)于指針類型论颅,所指對(duì)象的const修飾不能去掉哎垦。
const int* const a = ...;
f(a) // T 推導(dǎo)為 const int*
auto x = a; // x 的類型為 const int*
其實(shí)不需要特別記憶,只要記住一點(diǎn):在能編譯通過的前提下恃疯,去掉沒必要的const和&符號(hào)即可:
const int& a = ...;
// 以下語句能否編譯通過漏设?
const int& x = a; // OK
int& x = a; // 編譯不通過,const int& 類型不可轉(zhuǎn)為int&
const int x = a; // OK
int x = a; // OK今妄,且去掉了多余的const和&符號(hào)
// 因此郑口,auto x = a; 中,x類型推導(dǎo)為 int
const int* const b = ...;
// 以下語句能否編譯通過盾鳞?
int* const x = b; // 編譯不通過
const int* const x = b; // OK
const int* x = b; // OK犬性,且去除了多余的const
// 因此,auto x = b; 中腾仅,x類型推導(dǎo)為 const int*
情況2. 加了引用
也就是T&的形式:
// 模板函數(shù):
template <typename T>
f(T& t) {...}
// auto聲明:
auto& x = ...;
這種情況則需要保持原有的const乒裆,舉例來看:
const int a = 1;
f(a); // T 推導(dǎo)為 const int,所以形參t類型為 const int&
auto& x = a; // x類型推導(dǎo)為 const int
保留const的原因也很好理解推励,因?yàn)檫@里是引用傳遞鹤耍,丟掉const會(huì)導(dǎo)致可以修改原來的值,同樣編譯不通過:
const int a = 1;
int& x = a; // 編譯不通過
const int& x = a; // OK
情況2.2 萬能引用
如果加了兩個(gè)引用符號(hào) &&验辞,則可以根據(jù)情況推導(dǎo)為普通的引用稿黄,或者右值引用,因此也叫做萬能引用:
// 模板函數(shù):
template <typename T>
f(T&& t) {...} // &&表示萬能引用
// auto聲明:
auto&& x = ...; // &&表示萬能引用
// 舉例:
int a = 1;
auto&& x = a; // x的類型是 int&
auto&& x = 1; // x的類型是 int&& 右值引用
其他情況
經(jīng)過上面的分析跌造,我們得出結(jié)論杆怕,對(duì)于模板類型的推導(dǎo),其實(shí)只要能替換成普通函數(shù)壳贪,并去掉不必要的const和引用&陵珍,就可以得到推導(dǎo)結(jié)果。
書中特別提到了數(shù)組作為函數(shù)實(shí)參的特殊情況撑碴,但實(shí)際上這并不算什么特殊情況撑教。我們先了解下普通函數(shù),數(shù)組作為參數(shù)的情況醉拓。
在普通函數(shù)簽名中伟姐,形參可以寫成數(shù)組的形式收苏,但實(shí)際上類型還是指針。二者最大的區(qū)別是:指針并不會(huì)包含數(shù)組長(zhǎng)度信息愤兵。這也是為什么函數(shù)以數(shù)組作為參數(shù)時(shí)鹿霸,一般還要再傳入數(shù)組長(zhǎng)度參數(shù)。
int getlen(char a[]) { // 等價(jià)于 int* a 或 int a[1]
return sizeof(a);
}
int main() {
char a[2];
cout << sizeof(a); // 輸出 2 即兩個(gè)char所占的空間
cout << getlen(a); // 輸出 8 即int*指針?biāo)嫉目臻g
}
其實(shí)秆乳,如果想要保留數(shù)組的長(zhǎng)度信息懦鼠,也有辦法,那就是使用“數(shù)組的引用”作為函數(shù)參數(shù):
int getlen(char (&a)[2]) { // a是數(shù)組char[2] 的引用類型
return sizeof(a); // 返回2
}
注意屹堰,形參中需指定長(zhǎng)度肛冶,且需要與實(shí)參的數(shù)組長(zhǎng)度一致。
而對(duì)于模板函數(shù)其實(shí)是一樣的扯键,可以使用數(shù)組作為參數(shù)睦袖,這樣實(shí)際上的類型是指針;也可以使用“數(shù)組的引用”作為參數(shù)荣刑,保留長(zhǎng)度信息馅笙。
template<typename T>
int f(T t) {
return sizeof (t);
}
template<typename T>
int g(T& t) {
return sizeof(t);
}
char a[2];
cout << f(a) << endl; // 輸出 8, 即指針的長(zhǎng)度,t被推導(dǎo)為char*類型
cout << g(a) << endl; // 輸出 2, t被推導(dǎo)為 char(&)[2] 類型
auto x = a; // x類型推導(dǎo)為 char*
auto& y = a; // y類型推導(dǎo)為 char(&)[2]
仔細(xì)對(duì)比后發(fā)現(xiàn)厉亏,其實(shí)這和普通函數(shù)的規(guī)則完全一致董习,即:數(shù)組會(huì)退化為指針,數(shù)組引用則不會(huì)爱只。
有趣的是皿淋,利用這一特性,我們可以寫一個(gè)函數(shù)虱颗,獲得數(shù)據(jù)長(zhǎng)度:
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T(&)[N]) {
return N;
}
char a[2];
// 聲明一個(gè)與數(shù)組a長(zhǎng)度相同的array
std::array<char, arraySize(a)> arr{};
arraySize聲明中的constexpr表示:返回的N是編譯期常量沥匈。這樣才可以用在array的聲明中蔗喂。
auto與模板唯一的區(qū)別
前面的每個(gè)例子中忘渔,都同時(shí)包含了模板與auto的類型推導(dǎo),它們二者的推導(dǎo)結(jié)果都一摸一樣缰儿。二者唯一的一點(diǎn)不同是:auto支持初始化表達(dá)式畦粮,而模板不支持:
template<typename T>
void f(T param);
f({1,2,3}); // 錯(cuò)誤,無法推導(dǎo)
auto x = {1,2,3}; // OK乖阵,x類型推導(dǎo)為std::initializer_list<int>
auto y = {1,2,3.0}; // 錯(cuò)誤宣赔,包含int和double,無法推導(dǎo)
int[] z = {1,2,3}; // OK
在上例中瞪浸,模板與auto出現(xiàn)區(qū)別的根本原因在于儒将,C++11中為了支持統(tǒng)一初始化,可以用這種語法進(jìn)行聲明对蒲;而函數(shù)參數(shù)則沒有這樣的語法钩蚊。
當(dāng)然贡翘,這樣推導(dǎo)的結(jié)果總是std::initializer_list<T>的類型,往往不是我們想要的結(jié)果砰逻,所以還是盡量去避免這樣使用鸣驱。
auto使用的優(yōu)劣
大多數(shù)時(shí)候,優(yōu)先使用auto
使用auto的好處包括:
- 簡(jiǎn)化一長(zhǎng)串復(fù)雜的類型
- 避免忘記初始化(使用auto未初始化會(huì)產(chǎn)生編譯錯(cuò)誤)
- 類型變更時(shí)可以自適應(yīng)
- 能夠?yàn)閘ambda表達(dá)式聲明變量蝠咆,且優(yōu)于std::function踊东,不需要堆內(nèi)存
- 避免由于類型寫錯(cuò)導(dǎo)致額外的運(yùn)行開銷
關(guān)于最后一點(diǎn),額外的運(yùn)行開銷刚操,舉例說明一下:
std::unordered_map<std::string, int> m;
// 不使用auto進(jìn)行遍歷闸翅,代碼出現(xiàn)瑕疵:
for (const std::pair<std::string, int>& p : m) {
//... 對(duì)p進(jìn)行操作
}
// 使用auto:
for (const auto& p : m) {
// ...
}
在for循環(huán)中,我們本意并不希望發(fā)生任何拷貝菊霜,因此使用了const引用的形式聲明p缎脾,但很可惜,在代碼運(yùn)行中占卧,仍然會(huì)發(fā)生拷貝遗菠。為什么呢?
原因是华蜒,m中元素正確類型是:std::pair<const std::string, int>辙纬,const不可以丟掉。由于這和代碼中p的類型不匹配叭喜,編譯器就會(huì)將其拷貝一份贺拣,并進(jìn)行類型轉(zhuǎn)換。所以捂蕴,由于程序員對(duì)p類型的判斷失誤譬涡,導(dǎo)致運(yùn)行期額外的性能開銷。
而使用auto時(shí)啥辨,則永遠(yuǎn)會(huì)推斷出正確的類型涡匀,可以完全避免這個(gè)問題。
auto使用的坑
有一種情況溉知,使用auto無法得到我們想要的類型陨瘩,這甚至可能會(huì)導(dǎo)致十分隱蔽的問題。例如:
std::vector<bool> GetVec() {
return {true};
}
void HandleBool(bool b) {
cout << b;
}
int main() {
auto b = GetVec()[0]; // 這里auto如果換成bool级乍,一切正常
HandleBool(b);
}
本例中舌劳,我們將std::vector<bool>的operator[]操作結(jié)果賦值給b,期望變量b是bool類型玫荣,并在HandlerBool中進(jìn)行打印甚淡。然而實(shí)際運(yùn)行發(fā)現(xiàn),打印的值往往不符合預(yù)期捅厂。
這是為什么贯卦?原因在于底挫,b的類型并沒有推斷為bool,而是一個(gè)std::vector<bool>::reference類型的復(fù)雜對(duì)象脸侥,其中包含一個(gè)指針建邓,指向vector<bool>中的某個(gè)位置。
之所以這樣做睁枕,是因?yàn)関ector內(nèi)部針對(duì)bool類型做了一些優(yōu)化官边。正常情況下,這個(gè)對(duì)象可以正確轉(zhuǎn)化成bool類型外遇。但是注簿,在上例中,GetVec返回的vector在執(zhí)行完這一行代碼后已經(jīng)銷毀跳仿,b對(duì)象中的指針則成為野指針诡渴,在調(diào)用HandlerBool時(shí),無法正確轉(zhuǎn)化成bool類型菲语。
在本例中妄辩,std::vector<bool>::reference類型我們稱之為代理類,它可以模擬另一種類型山上。類似的眼耀,智能指針shared_ptr、unique_ptr也是代理類佩憾,它們可以模擬普通指針哮伟。但本例中的代理類更加隱蔽,我們難以察覺妄帘,才會(huì)出現(xiàn)上述問題楞黄。
所以,對(duì)于這種隱形代理類抡驼,需要根據(jù)情況避免使用auto來聲明鬼廓。