亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入解析C++中的auto自動類型推導

 更新時間:2024年04月11日 16:28:36   作者:iShare_愛分享  
C++標準委員會在C++11標準中改變了auto關(guān)鍵字的語義,使它變成一個類型占位符,允許在定義變量時不必明確寫出確切的類型,讓編譯器在編譯期間根據(jù)初始值自動推導出它的類型,下面我們就來看看auto自動類型推導的推導規(guī)則吧

關(guān)鍵字auto在C++98中的語義是定義一個自動生命周期的變量,但因為定義的變量默認就是自動變量,因此這個關(guān)鍵字幾乎沒有人使用。于是C++標準委員會在C++11標準中改變了auto關(guān)鍵字的語義,使它變成一個類型占位符,允許在定義變量時不必明確寫出確切的類型,讓編譯器在編譯期間根據(jù)初始值自動推導出它的類型。這篇文章我們來解析auto自動類型推導的推導規(guī)則,以及使用auto有哪些優(yōu)點,還有羅列出自C++11重新定義了auto的含義以后,在之后發(fā)布的C++14、C++17、C++20標準對auto的更新、增強的功能,以及auto有哪些使用限制。

推導規(guī)則

我們將以下面的形式來討論:

auto var = expr;

這時auto代表了變量var的類型,除此形式之外還可以再加上一些類型修飾詞,如:

const auto var = expr;
// 或者
const auto& var = expr;

這時變量var的類型是const auto或者const auto&,const也可以換成volatile修飾詞,這兩個稱為CV修飾詞,引用&也可以換成指針,如const auto,這時明確指出定義的是指針類型。

根據(jù)上面定義的形式,根據(jù)“=”左邊auto的修飾情況分為三種情形:

規(guī)則一:只有auto的情況,既非引用也非指針,表示按值初始化

如下的定義:

auto i = 1;	// i為int
auto d = 1.0;	// d為double

變量i將被推導為int類型,變量d將被推導為double類型,這時是根據(jù)“=”右邊的表達式的值來推導出auto的類型,并將它們的值復制到左邊的變量i和d中,因為是將右邊expr表達式的值復制到左邊變量中,所以右邊表達式的CV(const和volatile)屬性將會被忽略掉,如下的代碼:

const int ci = 1;
auto i = ci;		// i為int

盡管ci是有const修飾的常量,但是變量i的類型是int類型,而非const int,因為此時i拷貝了ci的值,i和ci是兩個不相關(guān)的變量,分別有不同的存儲空間,變量ci不可修改的屬性不代表變量i也不可修改。

當使用auto在同一條語句中定義多個變量時,變量的初始值的類型必須要統(tǒng)一,否則將無法推導出類型而導致編譯錯誤:

auto i = 1, j = 2;	// i和j都為int
auto i = 1, j = 2.0;	// 編譯錯誤,i為int,j為double

規(guī)則二:形式如auto&或auto*,表示定義引用或者指針

當定義變量時使用如auto&或auto*的類型修飾,表示定義的是一個引用類型或者指針類型,這時右邊的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被推導為int,因此i的類型為int&。(2)語句中auto被推導為const int,ci的類型為const int &,因為ci是對cx的引用,而cx是一個const修飾的常量,因此對它的引用也必須是常量引用。(3)語句中的auto被推導為const int,pi的類型為const int*,rx的const屬性將得到保留。

除了下面即將要講到的第三種情況外,auto都不會推導出結(jié)果是引用的類型,如果要定義為引用類型,就要像上面那樣明確地寫出來,但是auto可以推導出來是指針類型,也就是說就算沒有明確寫出auto*,如果expr的類型是指針類型的話,auto則會被推導為指針類型,這時expr的const屬性也會得到保留,如下的例子:

int i = 1;
auto pi = &i;	// pi為int*
const char word[] = "Hello world!";
auto str = word;	// str為const char*

pi被推導出來的類型為int,而str被推導出來的類型為const char。

規(guī)則三:形式如auto&&,表示萬能引用

當以auto&&的形式出現(xiàn)時,它表示的是萬能引用而非右值引用,這時將視expr的類型分為兩種情況,如果expr是個左值,那么它推導出來的結(jié)果是一個左值引用,這也是auto被推導為引用類型的唯一情形。而如果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的類型被推導為int&。(2)語句中的cx類型是const int且是左值,因此ref2的類型被推導為const int&。(3)語句中右側(cè)的2是一個右值且類型為int,所以ref3的類型被推導為int&&。

上面根據(jù)“=”左側(cè)的auto的形式歸納討論了三種情形下的推導規(guī)則,接下來根據(jù)“=”右側(cè)的expr的不同情況來討論推導規(guī)則:

expr是一個引用

如果expr是一個引用,那么它的引用屬性將被忽略,因為我們使用的是它引用的對象,而非這個引用本身,然后再根據(jù)上面的三種推導規(guī)則來推導,如下的定義:

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被推導為int類型。(2)語句中的crx是個常量引用,它和(1)語句的情況一樣,這里只是復制它所引用的對象的值,它的const屬性跟變量j沒有關(guān)系,所以變量j的類型為int。(3)語句里的ri的類型修飾是auto&,所以應用上面的第二條規(guī)則,它是一個引用類型,而且crx的const屬性將得到保留,因此ri的類型推導為const int&。

expr是初始化列表

當expr是一個初始化列表時,分為兩種情況而定:

auto var = {};	// (1)
// 或者
auto var{};	// (2)

當使用第一種方式時,var將被推導為initializer_list類型,這時無論花括號內(nèi)是單個元素還是多個元素,都是推導為initializer_list類型,而且如果是多個元素,每個元素的類型都必須要相同,否則將編譯錯誤,如下例子:

auto x1 = {1, 2, 3, 4};		// x1為initializer_list<int>
auto x2 = {1, 2, 3, 4.0};	// 編譯錯誤

x1的類型為initializer_list,這里將經(jīng)過兩次類型推導,第一次是將x1推導為initializer_list類型,第二次利用花括號內(nèi)的元素推導出元素的類型T為int類型。x2的定義將會引起編譯錯誤,因為x2雖然推導為initializer_list類型,但是在推導T的類型時,里面的元素的類型不統(tǒng)一,導致無法推導出T的類型,引起編譯錯誤。

當使用第二種方式時,var的類型被推導為花括號內(nèi)元素的類型,花括號內(nèi)必須為單元素,如下:

auto x1{1};	// x1為int
auto x2{1.0};	// x2為double

x1的類型推導為int,x2的類型推導為double。這種形式下花括號內(nèi)必須為單元素,如果有多個元素將會編譯錯誤,如:

auto x3{1, 2};	// 編譯錯誤

這個將導致編譯錯誤:error: initializer for variable 'x3' with type 'auto' contains multiple expressions。

expr是數(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ù)組將退化為指針類型,如下:

const char name[] = "My Name";
auto str = name;	// str為const char*

str被推導為const char*類型,盡管name的類型為const char[8]。

但如果定義變量的形式是引用的話,根據(jù)上面的第二種規(guī)則,它將被推導為數(shù)組原本的類型:

const char name[] = "My Name";
auto& str = name;	// str為const char (&)[8]

這時auto被推導為const char [8],str是一個指向數(shù)組的引用,類型為const char (&)[8]。

當expr是函數(shù)時,它的規(guī)則和數(shù)組的情況類似,按值初始化時將退化為函數(shù)指針,如為引用時將為函數(shù)的引用,如下例子:

void func(int, double) {}
auto f1 = func;		// f1為void (*)(int, double)
auto& f2 = func;	// f2為void (&)(int, double)

f1的類型推導出來為void (*)(int, double),f2的類型推導出來為void (&)(int, double)。

expr是條件表達式語句

當expr是一個條件表達式語句時,條件表達式根據(jù)條件可能返回不同類型的值,這時編譯器將會使用更大范圍的類型來作為推導結(jié)果的類型,如:

auto i =  condition ? 1 : 2.0;	// i為double

無論condition的結(jié)果是true還是false,i的類型都將被推導為double類型。

使用auto的好處

強制初始化的作用

當你定義一個變量時,可以這樣寫:

int i;

這樣寫編譯是能夠通過的,但是卻有安全隱患,比如在局部代碼中定義了這個變量,然后又接著使用它了,可能面臨未初始化的風險。但如果你這樣寫:

auto i;

這樣是編譯不通過的,因為變量i缺少初始值,你必須給i指定初始值,如下:

auto i = 0;

必須給變量i初始值才能編譯通過,這就避免了使用未初始化變量的風險。

定義小范圍內(nèi)的局部變量時

在小范圍的局部代碼中定義一個臨時變量,對理解整體代碼不會造成困擾的,比如:

for (auto i = 1; i < size(); ++i) {}

或者是基于范圍的for循環(huán)的代碼,只是想要遍歷容器中的元素,對于元素的類型不關(guān)心,如:

std::vector<int> v = {};
for (const auto& i : v) {}

減少冗余代碼

當變量的類型非常長時,明確寫出它的類型會使代碼變得又臃腫又難懂,而實際上我們并不關(guān)心它的具體類型,如:

std::map<std::string, int> m;
for (std::map<std::string, int>::iterator it = m.begin(); it != m.end(); ++it) {}

上面的代碼非常長,造成閱讀代碼的不便,對增加理解代碼的邏輯也沒有什么好處,實際上我們并不關(guān)心it的實際類型,這時使用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);

無法寫出的類型

如果說上面的代碼雖然難懂和難寫,畢竟還可以寫出來,但有時在某些情況下卻無法寫出來,比如用一個變量來存儲lambda表達式時,我們無法寫出lambda表達式的類型是什么,這時可以使用auto來自動推導:

auto compare = [](int p1, int p2) { return p1 < p2; }

避免對類型硬編碼

除了上面提到的可以減少代碼的冗余之外,使用auto也可以避免對類型的硬編碼,也就是說不寫死變量的類型,讓編譯器自動推導,如果我們要修改代碼,就不用去修改相應的類型,比如我們將一種容器的類型改為另一種容器,迭代器的類型不需要修改,如:

std::map<std::string, int> m = { ... };
auto it = m.begin();
// 修改為無序容器時
std::unordered_map<std::string, int> m = { ... };
auto it = m.begin();

C++標準庫里的容器大部分的接口都是相同的,泛型算法也能應用于大部分的容器,所以對于容器的具體類型并不是很重要,當根據(jù)業(yè)務的需要更換不同的容器時,使用auto可以很方便的修改代碼。

跨平臺可移植性

假如你的代碼中定義了一個vector,然后想要獲取vector的元素的大小,這時你調(diào)用了成員函數(shù)size來獲取,此時應該定義一個什么類型的變量來承接它的返回值?vector的成員函數(shù)size的原型如下:

size_type size() const noexcept;

size_type是vector內(nèi)定義的類型,標準庫對它的解釋是“an unsigned integral type that can represent any non-negative value of difference_type”,于是你認為用unsigned類型就可以了,于是寫下如下代碼:

std::vector<int> v;
unsigned sz = v.size();

這樣寫可能會導致安全隱患,比如在32位的系統(tǒng)上,unsigned的大小是4個字節(jié),size_type的大小也是4個字節(jié),但是在64位的系統(tǒng)上,unsigned的大小是4個字節(jié),而size_type的大小卻是8個字節(jié)。這意味著原本在32位系統(tǒng)上運行良好的代碼可能在64位的系統(tǒng)上運行異常,如果這里用auto來定義變量,則可以避免這種問題。

避免寫錯類型

還有一種似是而非的問題,就是你的代碼看起來沒有問題,編譯也沒有問題,運行也正常,但是效率可能不如預期的高,比如有以下的代碼:

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的類型是前者,這會導致編譯器想盡辦法來將m中的元素(類型為std::pair<const std::string, int>)轉(zhuǎn)換成std::pair<std::string, int>類型,因此編譯器會拷貝m中的所有元素到臨時對象,然后再讓p引用到這些臨時對象,每迭代一次,臨時對象就被析構(gòu)一次,這就導致了無故拷貝了那么多次對象和析構(gòu)臨時對象,效率上當然會大打折扣。如果你用auto來替代上面的定義,則完全可以避免這樣的問題發(fā)生,如:

for (const auto&amp; p : m) {}

新標準新增功能

自動推導函數(shù)的返回值類型(C++14)

C++14標準支持了使用auto來推導函數(shù)的返回值類型,這樣就不必明確寫出函數(shù)返回值的類型,如下的代碼:

template&lt;typename T1, typename T2&gt;
auto add(T1 a, T2 b) {
    return a + b;
}

int main() {
    auto i = add(1, 2);
}

不用管傳入給add函數(shù)的參數(shù)的類型是什么,編譯器會自動推導出返回值的類型。

使用auto聲明lambda的形參(C++14)

C++14標準還支持了可以使用auto來聲明lambda表達式的形參,但普通函數(shù)的形參使用auto來聲明需要C++20標準才支持,下面會提到。如下面的例子:

auto sum = [](auto p1, auto p2) { return p1 + p2; };

這樣定義的lambda式有點像是模板,調(diào)用sum時會根據(jù)傳入的參數(shù)推導出類型,你可以傳入int類型參數(shù)也可以傳入double類型參數(shù),甚至也可以傳入自定義類型,如果自定義類型支持加法運算的話。

非類型模板形參的占位符(C++17)

C++17標準再次拓展了auto的功能,使得能夠作為非類型模板形參的占位符,如下的例子:

template&lt;auto N&gt;
void func() {
    std::cout &lt;&lt; N &lt;&lt; std::endl;
}

func&lt;1&gt;();	// N為int類型
func&lt;'c'&gt;();	// N為chat類型

但是要保證推導出來的類型是能夠作為模板形參的,比如推導出來是double類型,但模板參數(shù)不能接受是double類型時,則會導致編譯不通過。

結(jié)構(gòu)化綁定功能(C++17)

C++17標準中auto還支持了結(jié)構(gòu)化綁定的功能,這個功能有點類似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 &lt;&lt; "a=" &lt;&lt; a &lt;&lt; ", b=" &lt;&lt; b &lt;&lt; ", c=" &lt;&lt; c &lt;&lt; std::endl;

auto的推導功能從以前對單個變量進行類型推導擴展到可以對一組變量的推導,這樣可以讓我們省略了需要先聲明變量再處理結(jié)構(gòu)化對象的麻煩,特別是在for循環(huán)中遍歷容器時,如下:

std::map&lt;std::string, int&gt; m;
for (auto&amp; [k, v] : m) {
    std::cout &lt;&lt; k &lt;&lt; " =&gt; " &lt;&lt; v &lt;&lt; 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類型,默認是const char*,
                                	// char*類型是不支持加法的

這個看起來是不是和模板很像?但是寫法要比模板要簡單,通過查看生成的匯編代碼,看到編譯器的處理方式跟模板的處理方式是一樣的,也就是說上面的三個函數(shù)調(diào)用分別產(chǎn)生出了三個函數(shù)實例:

auto add&lt;int, int&gt;(int, int);
auto add&lt;double, double&gt;(double, double);
auto add&lt;std::__cxx11::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt;, std::__cxx11::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt; &gt;(std::__cxx11::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt;, std::__cxx11::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt;);

使用auto的限制

上面詳細列出了使用auto的好處和使用場景,但在有些地方使用auto還存在限制,下面也一并羅列出來。

類內(nèi)初始化成員時不能使用auto

在C++11標準中已經(jīng)支持了在類內(nèi)初始化數(shù)據(jù)成員,也就是說在定義類時,可以直接在類內(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標準之前,使用auto聲明靜態(tài)數(shù)據(jù)成員需要加上const修飾詞,這就給使用上造成了不便,因此在C++17標準中取消了這個限制:

class Object {
	static inline auto a = 1;	// 需要寫上inline修飾詞
};

函數(shù)無法返回initializer_list類型

雖然在C++14中支持了自動推導函數(shù)的返回值類型,但卻不支持返回的類型是initializer_list類型,因此下面的代碼將編譯不通過:

auto createList() {
    return {1, 2, 3};
}

編譯錯誤信息:error: cannot deduce return type from initializer list。

lambda式參數(shù)無法使用initializer_list類型

同樣地,在lambda式使用auto來聲明形參時,也不能給它傳遞initializer_list類型的參數(shù),如下代碼:

std::vector&lt;int&gt; v;
auto resetV = [&amp;v](const auto&amp; newV) { v = newV; };
resetV({1, 2, 3});

上面的代碼會編譯錯誤,無法使用參數(shù){1, 2, 3}來推導出newV的類型。

到此這篇關(guān)于深入解析C++中的auto自動類型推導的文章就介紹到這了,更多相關(guān)C++ auto自動類型推導內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論