深入解析C++中的auto自動類型推導(dǎo)
關(guān)鍵字auto在C++98中的語義是定義一個自動生命周期的變量,但因?yàn)槎x的變量默認(rèn)就是自動變量,因此這個關(guān)鍵字幾乎沒有人使用。于是C++標(biāo)準(zhǔn)委員會在C++11標(biāo)準(zhǔn)中改變了auto關(guān)鍵字的語義,使它變成一個類型占位符,允許在定義變量時(shí)不必明確寫出確切的類型,讓編譯器在編譯期間根據(jù)初始值自動推導(dǎo)出它的類型。這篇文章我們來解析auto自動類型推導(dǎo)的推導(dǎo)規(guī)則,以及使用auto有哪些優(yōu)點(diǎn),還有羅列出自C++11重新定義了auto的含義以后,在之后發(fā)布的C++14、C++17、C++20標(biāo)準(zhǔn)對auto的更新、增強(qiáng)的功能,以及auto有哪些使用限制。
推導(dǎo)規(guī)則
我們將以下面的形式來討論:
auto var = expr;
這時(shí)auto代表了變量var的類型,除此形式之外還可以再加上一些類型修飾詞,如:
const auto var = expr; // 或者 const auto& var = expr;
這時(shí)變量var的類型是const auto或者const auto&,const也可以換成volatile修飾詞,這兩個稱為CV修飾詞,引用&也可以換成指針,如const auto,這時(shí)明確指出定義的是指針類型。
根據(jù)上面定義的形式,根據(jù)“=”左邊auto的修飾情況分為三種情形:
規(guī)則一:只有auto的情況,既非引用也非指針,表示按值初始化
如下的定義:
auto i = 1; // i為int auto d = 1.0; // d為double
變量i將被推導(dǎo)為int類型,變量d將被推導(dǎo)為double類型,這時(shí)是根據(jù)“=”右邊的表達(dá)式的值來推導(dǎo)出auto的類型,并將它們的值復(fù)制到左邊的變量i和d中,因?yàn)槭菍⒂疫卐xpr表達(dá)式的值復(fù)制到左邊變量中,所以右邊表達(dá)式的CV(const和volatile)屬性將會被忽略掉,如下的代碼:
const int ci = 1; auto i = ci; // i為int
盡管ci是有const修飾的常量,但是變量i的類型是int類型,而非const int,因?yàn)榇藭r(shí)i拷貝了ci的值,i和ci是兩個不相關(guān)的變量,分別有不同的存儲空間,變量ci不可修改的屬性不代表變量i也不可修改。
當(dāng)使用auto在同一條語句中定義多個變量時(shí),變量的初始值的類型必須要統(tǒng)一,否則將無法推導(dǎo)出類型而導(dǎo)致編譯錯誤:
auto i = 1, j = 2; // i和j都為int auto i = 1, j = 2.0; // 編譯錯誤,i為int,j為double
規(guī)則二:形式如auto&或auto*,表示定義引用或者指針
當(dāng)定義變量時(shí)使用如auto&或auto*的類型修飾,表示定義的是一個引用類型或者指針類型,這時(shí)右邊的expr的CV屬性將不能被忽略,如下的定義:
int x = 1; const int cx = x; const int& rx = x; auto& i = x; // (1) i為int& auto& ci = cx; // (2) ci為const int& auto* pi = ℞ // (3) pi為const int*
(1)語句中auto被推導(dǎo)為int,因此i的類型為int&。(2)語句中auto被推導(dǎo)為const int,ci的類型為const int &,因?yàn)閏i是對cx的引用,而cx是一個const修飾的常量,因此對它的引用也必須是常量引用。(3)語句中的auto被推導(dǎo)為const int,pi的類型為const int*,rx的const屬性將得到保留。
除了下面即將要講到的第三種情況外,auto都不會推導(dǎo)出結(jié)果是引用的類型,如果要定義為引用類型,就要像上面那樣明確地寫出來,但是auto可以推導(dǎo)出來是指針類型,也就是說就算沒有明確寫出auto*,如果expr的類型是指針類型的話,auto則會被推導(dǎo)為指針類型,這時(shí)expr的const屬性也會得到保留,如下的例子:
int i = 1; auto pi = &i; // pi為int* const char word[] = "Hello world!"; auto str = word; // str為const char*
pi被推導(dǎo)出來的類型為int,而str被推導(dǎo)出來的類型為const char。
規(guī)則三:形式如auto&&,表示萬能引用
當(dāng)以auto&&的形式出現(xiàn)時(shí),它表示的是萬能引用而非右值引用,這時(shí)將視expr的類型分為兩種情況,如果expr是個左值,那么它推導(dǎo)出來的結(jié)果是一個左值引用,這也是auto被推導(dǎo)為引用類型的唯一情形。而如果expr是個右值,那么將依據(jù)上面的第一種情形的規(guī)則。如下的例子:
int x = 1; const int cx = x; auto&& ref1 = x; // (1) ref1為int& auto&& ref2 = cx; // (2) ref2為const int& auto&& ref3 = 2; // (3) ref3為int&&
(1)語句中x的類型是int且是左值,所以ref1的類型被推導(dǎo)為int&。(2)語句中的cx類型是const int且是左值,因此ref2的類型被推導(dǎo)為const int&。(3)語句中右側(cè)的2是一個右值且類型為int,所以ref3的類型被推導(dǎo)為int&&。
上面根據(jù)“=”左側(cè)的auto的形式歸納討論了三種情形下的推導(dǎo)規(guī)則,接下來根據(jù)“=”右側(cè)的expr的不同情況來討論推導(dǎo)規(guī)則:
expr是一個引用
如果expr是一個引用,那么它的引用屬性將被忽略,因?yàn)槲覀兪褂玫氖撬玫膶ο?,而非這個引用本身,然后再根據(jù)上面的三種推導(dǎo)規(guī)則來推導(dǎo),如下的定義:
int x = 1; int &rx = x; const int &crx = x; auto i = rx; // (1) i為int auto j = crx; // (2) j為int auto& ri = crx; // (3) ri為const int&
(1)語句中rx雖然是個引用,但是這里是使用它引用的對象的值,所以根據(jù)上面的第一條規(guī)則,這里i被推導(dǎo)為int類型。(2)語句中的crx是個常量引用,它和(1)語句的情況一樣,這里只是復(fù)制它所引用的對象的值,它的const屬性跟變量j沒有關(guān)系,所以變量j的類型為int。(3)語句里的ri的類型修飾是auto&,所以應(yīng)用上面的第二條規(guī)則,它是一個引用類型,而且crx的const屬性將得到保留,因此ri的類型推導(dǎo)為const int&。
expr是初始化列表
當(dāng)expr是一個初始化列表時(shí),分為兩種情況而定:
auto var = {}; // (1) // 或者 auto var{}; // (2)
當(dāng)使用第一種方式時(shí),var將被推導(dǎo)為initializer_list類型,這時(shí)無論花括號內(nèi)是單個元素還是多個元素,都是推導(dǎo)為initializer_list類型,而且如果是多個元素,每個元素的類型都必須要相同,否則將編譯錯誤,如下例子:
auto x1 = {1, 2, 3, 4}; // x1為initializer_list<int> auto x2 = {1, 2, 3, 4.0}; // 編譯錯誤
x1的類型為initializer_list,這里將經(jīng)過兩次類型推導(dǎo),第一次是將x1推導(dǎo)為initializer_list類型,第二次利用花括號內(nèi)的元素推導(dǎo)出元素的類型T為int類型。x2的定義將會引起編譯錯誤,因?yàn)閤2雖然推導(dǎo)為initializer_list類型,但是在推導(dǎo)T的類型時(shí),里面的元素的類型不統(tǒng)一,導(dǎo)致無法推導(dǎo)出T的類型,引起編譯錯誤。
當(dāng)使用第二種方式時(shí),var的類型被推導(dǎo)為花括號內(nèi)元素的類型,花括號內(nèi)必須為單元素,如下:
auto x1{1}; // x1為int auto x2{1.0}; // x2為double
x1的類型推導(dǎo)為int,x2的類型推導(dǎo)為double。這種形式下花括號內(nèi)必須為單元素,如果有多個元素將會編譯錯誤,如:
auto x3{1, 2}; // 編譯錯誤
這個將導(dǎo)致編譯錯誤:error: initializer for variable 'x3' with type 'auto' contains multiple expressions。
expr是數(shù)組或者函數(shù)
數(shù)組在某些情況會退化成一個指向數(shù)組首元素的指針,但其實(shí)數(shù)組類型和指針類型并不相同,如下的定義:
const char name[] = "My Name"; const char* str = name;
數(shù)組name的類型是const char[8],而str的類型為const char*,在某些語義下它們可以互換,如在第一種規(guī)則下,expr是數(shù)組時(shí),數(shù)組將退化為指針類型,如下:
const char name[] = "My Name"; auto str = name; // str為const char*
str被推導(dǎo)為const char*類型,盡管name的類型為const char[8]。
但如果定義變量的形式是引用的話,根據(jù)上面的第二種規(guī)則,它將被推導(dǎo)為數(shù)組原本的類型:
const char name[] = "My Name"; auto& str = name; // str為const char (&)[8]
這時(shí)auto被推導(dǎo)為const char [8],str是一個指向數(shù)組的引用,類型為const char (&)[8]。
當(dāng)expr是函數(shù)時(shí),它的規(guī)則和數(shù)組的情況類似,按值初始化時(shí)將退化為函數(shù)指針,如為引用時(shí)將為函數(shù)的引用,如下例子:
void func(int, double) {} auto f1 = func; // f1為void (*)(int, double) auto& f2 = func; // f2為void (&)(int, double)
f1的類型推導(dǎo)出來為void (*)(int, double),f2的類型推導(dǎo)出來為void (&)(int, double)。
expr是條件表達(dá)式語句
當(dāng)expr是一個條件表達(dá)式語句時(shí),條件表達(dá)式根據(jù)條件可能返回不同類型的值,這時(shí)編譯器將會使用更大范圍的類型來作為推導(dǎo)結(jié)果的類型,如:
auto i = condition ? 1 : 2.0; // i為double
無論condition的結(jié)果是true還是false,i的類型都將被推導(dǎo)為double類型。
使用auto的好處
強(qiáng)制初始化的作用
當(dāng)你定義一個變量時(shí),可以這樣寫:
int i;
這樣寫編譯是能夠通過的,但是卻有安全隱患,比如在局部代碼中定義了這個變量,然后又接著使用它了,可能面臨未初始化的風(fēng)險(xiǎn)。但如果你這樣寫:
auto i;
這樣是編譯不通過的,因?yàn)樽兞縤缺少初始值,你必須給i指定初始值,如下:
auto i = 0;
必須給變量i初始值才能編譯通過,這就避免了使用未初始化變量的風(fēng)險(xiǎn)。
定義小范圍內(nèi)的局部變量時(shí)
在小范圍的局部代碼中定義一個臨時(shí)變量,對理解整體代碼不會造成困擾的,比如:
for (auto i = 1; i < size(); ++i) {}
或者是基于范圍的for循環(huán)的代碼,只是想要遍歷容器中的元素,對于元素的類型不關(guān)心,如:
std::vector<int> v = {}; for (const auto& i : v) {}
減少冗余代碼
當(dāng)變量的類型非常長時(shí),明確寫出它的類型會使代碼變得又臃腫又難懂,而實(shí)際上我們并不關(guān)心它的具體類型,如:
std::map<std::string, int> m; for (std::map<std::string, int>::iterator it = m.begin(); it != m.end(); ++it) {}
上面的代碼非常長,造成閱讀代碼的不便,對增加理解代碼的邏輯也沒有什么好處,實(shí)際上我們并不關(guān)心it的實(shí)際類型,這時(shí)使用auto就使代碼變得簡潔:
for (auto it = m.begin(); it != m.end(); ++it) {}
再比如下面的例子:
std::unordered_multimap<int, int> m; std::pair<std::unordered_multimap<int, int>::iterator, std::unordered_multimap<int ,int>::iterator> range = m.equal_range(k);
對于上面的代碼簡直難懂,第一遍看還看不出來想代表的意思是什么,如果改為auto來寫,則一目了然,一看就知道是在定義一個變量:
auto range = m.equal_range(k);
無法寫出的類型
如果說上面的代碼雖然難懂和難寫,畢竟還可以寫出來,但有時(shí)在某些情況下卻無法寫出來,比如用一個變量來存儲lambda表達(dá)式時(shí),我們無法寫出lambda表達(dá)式的類型是什么,這時(shí)可以使用auto來自動推導(dǎo):
auto compare = [](int p1, int p2) { return p1 < p2; }
避免對類型硬編碼
除了上面提到的可以減少代碼的冗余之外,使用auto也可以避免對類型的硬編碼,也就是說不寫死變量的類型,讓編譯器自動推導(dǎo),如果我們要修改代碼,就不用去修改相應(yīng)的類型,比如我們將一種容器的類型改為另一種容器,迭代器的類型不需要修改,如:
std::map<std::string, int> m = { ... }; auto it = m.begin(); // 修改為無序容器時(shí) std::unordered_map<std::string, int> m = { ... }; auto it = m.begin();
C++標(biāo)準(zhǔn)庫里的容器大部分的接口都是相同的,泛型算法也能應(yīng)用于大部分的容器,所以對于容器的具體類型并不是很重要,當(dāng)根據(jù)業(yè)務(wù)的需要更換不同的容器時(shí),使用auto可以很方便的修改代碼。
跨平臺可移植性
假如你的代碼中定義了一個vector,然后想要獲取vector的元素的大小,這時(shí)你調(diào)用了成員函數(shù)size來獲取,此時(shí)應(yīng)該定義一個什么類型的變量來承接它的返回值?vector的成員函數(shù)size的原型如下:
size_type size() const noexcept;
size_type是vector內(nèi)定義的類型,標(biāo)準(zhǔn)庫對它的解釋是“an unsigned integral type that can represent any non-negative value of difference_type”,于是你認(rèn)為用unsigned類型就可以了,于是寫下如下代碼:
std::vector<int> v; unsigned sz = v.size();
這樣寫可能會導(dǎo)致安全隱患,比如在32位的系統(tǒng)上,unsigned的大小是4個字節(jié),size_type的大小也是4個字節(jié),但是在64位的系統(tǒng)上,unsigned的大小是4個字節(jié),而size_type的大小卻是8個字節(jié)。這意味著原本在32位系統(tǒng)上運(yùn)行良好的代碼可能在64位的系統(tǒng)上運(yùn)行異常,如果這里用auto來定義變量,則可以避免這種問題。
避免寫錯類型
還有一種似是而非的問題,就是你的代碼看起來沒有問題,編譯也沒有問題,運(yùn)行也正常,但是效率可能不如預(yù)期的高,比如有以下的代碼:
std::unordered_map<std::string, int> m = { ... }; for (const std::pair<std::string, int> &p : m) {}
這段代碼看起來完全沒有問題,編譯也沒有任何警告,但是卻暗藏隱患。原因是std::unordered_map容器的鍵值的類型是const的,所以std::pair的類型不是std::pair<std::string, int>而是std::pair<const std::string, int>。但是上面的代碼中定義p的類型是前者,這會導(dǎo)致編譯器想盡辦法來將m中的元素(類型為std::pair<const std::string, int>)轉(zhuǎn)換成std::pair<std::string, int>類型,因此編譯器會拷貝m中的所有元素到臨時(shí)對象,然后再讓p引用到這些臨時(shí)對象,每迭代一次,臨時(shí)對象就被析構(gòu)一次,這就導(dǎo)致了無故拷貝了那么多次對象和析構(gòu)臨時(shí)對象,效率上當(dāng)然會大打折扣。如果你用auto來替代上面的定義,則完全可以避免這樣的問題發(fā)生,如:
for (const auto& p : m) {}
新標(biāo)準(zhǔn)新增功能
自動推導(dǎo)函數(shù)的返回值類型(C++14)
C++14標(biāo)準(zhǔn)支持了使用auto來推導(dǎo)函數(shù)的返回值類型,這樣就不必明確寫出函數(shù)返回值的類型,如下的代碼:
template<typename T1, typename T2> auto add(T1 a, T2 b) { return a + b; } int main() { auto i = add(1, 2); }
不用管傳入給add函數(shù)的參數(shù)的類型是什么,編譯器會自動推導(dǎo)出返回值的類型。
使用auto聲明lambda的形參(C++14)
C++14標(biāo)準(zhǔn)還支持了可以使用auto來聲明lambda表達(dá)式的形參,但普通函數(shù)的形參使用auto來聲明需要C++20標(biāo)準(zhǔn)才支持,下面會提到。如下面的例子:
auto sum = [](auto p1, auto p2) { return p1 + p2; };
這樣定義的lambda式有點(diǎn)像是模板,調(diào)用sum時(shí)會根據(jù)傳入的參數(shù)推導(dǎo)出類型,你可以傳入int類型參數(shù)也可以傳入double類型參數(shù),甚至也可以傳入自定義類型,如果自定義類型支持加法運(yùn)算的話。
非類型模板形參的占位符(C++17)
C++17標(biāo)準(zhǔn)再次拓展了auto的功能,使得能夠作為非類型模板形參的占位符,如下的例子:
template<auto N> void func() { std::cout << N << std::endl; } func<1>(); // N為int類型 func<'c'>(); // N為chat類型
但是要保證推導(dǎo)出來的類型是能夠作為模板形參的,比如推導(dǎo)出來是double類型,但模板參數(shù)不能接受是double類型時(shí),則會導(dǎo)致編譯不通過。
結(jié)構(gòu)化綁定功能(C++17)
C++17標(biāo)準(zhǔn)中auto還支持了結(jié)構(gòu)化綁定的功能,這個功能有點(diǎn)類似tuple類型的tie函數(shù),它可以分解結(jié)構(gòu)化類型的數(shù)據(jù),把多個變量綁定到結(jié)構(gòu)化對象內(nèi)部的對象上,在沒有支持這個功能之前,要分解tuple里的數(shù)據(jù)需要這樣寫:
tuple x{1, "hello"s, 5.0}; itn a; std::string b; double c; std::tie(a, b, c) = x; // a=1, b="hello", c=5.0
在C++17之后可以使用auto來這樣寫:
tuple x{1, "hello"s, 5.0}; auto [a, b, c] = x; // 作用如上 std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
auto的推導(dǎo)功能從以前對單個變量進(jìn)行類型推導(dǎo)擴(kuò)展到可以對一組變量的推導(dǎo),這樣可以讓我們省略了需要先聲明變量再處理結(jié)構(gòu)化對象的麻煩,特別是在for循環(huán)中遍歷容器時(shí),如下:
std::map<std::string, int> m; for (auto& [k, v] : m) { std::cout << k << " => " << v << std::endl; }
使用auto聲明函數(shù)的形參(C++20)
之前提到無法在普通函數(shù)中使用auto來聲明形參,這個功能在C++20中也得到了支持。你終于可以寫下這樣的代碼了:
auto add (auto p1, auto p2) { return p1 + p2; }; auto i = add(1, 2); auto d = add(5.0, 6.0); auto s = add("hello"s, "world"s); // 必須要寫上s,表示是string類型,默認(rèn)是const char*, // char*類型是不支持加法的
這個看起來是不是和模板很像?但是寫法要比模板要簡單,通過查看生成的匯編代碼,看到編譯器的處理方式跟模板的處理方式是一樣的,也就是說上面的三個函數(shù)調(diào)用分別產(chǎn)生出了三個函數(shù)實(shí)例:
auto add<int, int>(int, int); auto add<double, double>(double, double); auto add<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >);
使用auto的限制
上面詳細(xì)列出了使用auto的好處和使用場景,但在有些地方使用auto還存在限制,下面也一并羅列出來。
類內(nèi)初始化成員時(shí)不能使用auto
在C++11標(biāo)準(zhǔn)中已經(jīng)支持了在類內(nèi)初始化數(shù)據(jù)成員,也就是說在定義類時(shí),可以直接在類內(nèi)聲明數(shù)據(jù)成員的地方直接寫上它們的初始值,但是在這個情況下不能使用auto來聲明非靜態(tài)數(shù)據(jù)成員,比如:
class Object { auto a = 1; // 編譯錯誤。 };
上面的代碼會出現(xiàn)編譯錯誤:error: 'auto' not allowed in non-static class member。雖然不能支持聲明非靜態(tài)數(shù)據(jù)成員,但卻可以支持聲明靜態(tài)數(shù)據(jù)成員,在C++17標(biāo)準(zhǔn)之前,使用auto聲明靜態(tài)數(shù)據(jù)成員需要加上const修飾詞,這就給使用上造成了不便,因此在C++17標(biāo)準(zhǔn)中取消了這個限制:
class Object { static inline auto a = 1; // 需要寫上inline修飾詞 };
函數(shù)無法返回initializer_list類型
雖然在C++14中支持了自動推導(dǎo)函數(shù)的返回值類型,但卻不支持返回的類型是initializer_list類型,因此下面的代碼將編譯不通過:
auto createList() { return {1, 2, 3}; }
編譯錯誤信息:error: cannot deduce return type from initializer list。
lambda式參數(shù)無法使用initializer_list類型
同樣地,在lambda式使用auto來聲明形參時(shí),也不能給它傳遞initializer_list類型的參數(shù),如下代碼:
std::vector<int> v; auto resetV = [&v](const auto& newV) { v = newV; }; resetV({1, 2, 3});
上面的代碼會編譯錯誤,無法使用參數(shù){1, 2, 3}來推導(dǎo)出newV的類型。
到此這篇關(guān)于深入解析C++中的auto自動類型推導(dǎo)的文章就介紹到這了,更多相關(guān)C++ auto自動類型推導(dǎo)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)編碼轉(zhuǎn)換的示例代碼
這篇文章主要介紹了C++實(shí)現(xiàn)編碼轉(zhuǎn)換的示例代碼,幫助大家快捷的實(shí)現(xiàn)編碼轉(zhuǎn)換,感興趣的朋友可以了解下2020-08-08C++算法之海量數(shù)據(jù)處理方法的總結(jié)分析
本篇文章是對海量數(shù)據(jù)處理方法進(jìn)行了詳細(xì)的總結(jié)與分析,需要的朋友參考下2013-05-05C++中std::invalid_argument報(bào)錯解決
在C++編程中,std::invalid_argument是一個常見的異常,用于指示函數(shù)參數(shù)無效,文章詳細(xì)解析了這一異常的產(chǎn)生原因,并提供了多種解決策略,感興趣的可以了解一下2024-09-09基于Protobuf C++ serialize到char*的實(shí)現(xiàn)方法分析
本篇文章是對Protobuf C++ serialize到char*的實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下2013-05-05