using(别名)和range based for
using(別名)替代typedef
關鍵字
using
語法
別名聲明是具有下列語法的聲明:
using?標識符?attr(可選)?=?類型標識?;?(1)
template?<?模板形參列表?>using?標識符?attr(可選)?=?類型標識?;(2)?
attr(C++11)?-???可選的任意數量屬性的序列
標識符?-???此聲明引入的名字,它成為一個類型名?(1)?或一個模板名?(2)
模板形參列表??-???模板形參列表,同模板聲明
類型標識????-???抽象聲明符或其他任何合法的?類型標識(可以引入新類型,如?類型標識?中所注明)。類型標識?不能直接或間接涉指?標識符。注意,標識符的聲明點處于跟在?類型標識?之后的分號處。
解釋
大家都知道,在 C++ 中可以通過 typedef 重定義一個類型:
typedef unsigned int uint_t;被重定義的類型并不是一個新的類型,僅僅只是原有的類型取了一個新的名字。因此,下面這樣將不是合法的函數重載:
void func(unsigned int);void func(uint_t); // error: redefinition使用 typedef 重定義類型是很方便的,但它也有一些限制,比如,無法重定義一個模板。
想象下面這個場景:
我們需要的其實是一個固定以 std::string 為 key 的 map,它可以映射到 int 或另一個 std::string。然而這個簡單的需求僅通過 typedef 卻很難辦到。
因此,在 C++98/03 中往往不得不這樣寫:
一個雖然簡單但卻略顯煩瑣的 str_map 外敷類是必要的。這明顯讓我們在復用某些泛型代碼時非常難受。
現在,在 C++11 中終于出現了可以重定義一個模板的語法。請看下面的示例:
這里使用新的 using 別名語法定義了 std::map 的模板別名 str_map_t。比起前面使用外敷模板加 typedef 構建的 str_map,它完全就像是一個新的 map 類模板,因此,簡潔了很多。
實際上,using 的別名語法覆蓋了 typedef 的全部功能。先來看看對普通類型的重定義示例,將這兩種語法對比一下:
可以看到,在重定義普通類型上,兩種使用方法的效果是等價的,唯一不同的是定義語法。
typedef 的定義方法和變量的聲明類似:像聲明一個變量一樣,聲明一個重定義類型,之后在聲明之前加上 typedef 即可。這種寫法凸顯了 C/C++ 中的語法一致性,但有時卻會增加代碼的閱讀難度。比如重定義一個函數指針時:
與之相比,using 后面總是立即跟隨新標識符(Identifier),之后使用類似賦值的語法,把現有的類型(type-id)賦給新類型:
using func_t = void (*)(int, int);從上面的對比中可以發現,C++11 的 using 別名語法比 typedef 更加清晰。因為 typedef 的別名語法本質上類似一種解方程的思路。而 using 語法通過賦值來定義別名,和我們平時的思考方式一致。
下面再通過一個對比示例,看看新的 using 語法是如何定義模板別名的。
從示例中可以看出,通過 using 定義模板別名的語法,只是在普通類型別名語法的基礎上增加 template 的參數列表。使用 using 可以輕松地創建一個新的模板別名,而不需要像 C++98/03 那樣使用煩瑣的外敷模板。
需要注意的是,using 語法和 typedef 一樣,并不會創造新的類型。也就是說,上面示例中 C++11 的 using 寫法只是 typedef 的等價物。雖然 using 重定義的 func_t 是一個模板,但 func_t<int> 定義的 xx_2 并不是一個由類模板實例化后的類,而是 void(*)(int, int) 的別名。
因此,下面這樣寫:
同樣是無法實現重載的,func_t<int> 只是 void(*)(int, int) 類型的等價物。
細心的讀者可以發現,using 重定義的 func_t 是一個模板,但它既不是類模板也不是函數模板(函數模板實例化后是一個函數),而是一種新的模板形式:模板別名(alias template)。
其實,通過 using 可以輕松定義任意類型的模板表達方式。比如下面這樣:
type_t 實例化后的類型和它的模板參數類型等價。這里,type_t<int> 將等價于 int。
?
range based for
熟悉C#或者python的人都知道在C#和python中存在一種for的使用方法不需要明確給出容器的開始和結束條件,就可以遍歷整個容器。C++11吸取了他們的優點,引入了這種方法也就是基于范圍的For(Range-Based-For)。
值得一說的是,如果沒有range-based-for,我們還是可以遍歷容器的。它是對for的擴展。
比如C98時候我們可以這樣:
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };for (int i = 0; i < 10; i++) cout << arr[i];或者遍歷容器,這樣:
std::vector<int> vec {1,2,3,4,5,6,7,8,9,10};for?(std::vector<int>::iterator?itr?=?vec.begin();?itr?!=?vec.end();?itr++) std::cout << *itr;不過有了C++11, 我們就可以這樣:
std::vector<int> vec {1,2,3,4,5,6,7,8,9,10};for?(auto?n?:vec) std::cout << n; int?arr[10]?=?{?1,?2,?3,?4,?5,?6,?7,?8,?9,?10?};for?(auto?n?:?arr)????std::cout?<<?n;乍一看,是不是顯得代碼非常簡潔?確實,這可能也是為啥引入這個概念。不過我覺得range-based-for最主要的作用可能就是“一統江湖”。有了它,不管你是數組,map,vector還是其他容器, 都可以用它來遍歷。
下面看看它的語法以及解釋:
//語法屬性(可選) for ( 范圍聲明 : 范圍表達式 ) 循環語句//解釋{ auto && __range = 范圍表達式 ; for (auto __begin = 首表達式, __end = 尾表達式 ; __begin != __end; ++__begin) { 范圍聲明 = *__begin; 循環語句 }}range-based-for看起來那么美好,其實在應用的時候還是要有一些需要注意的事情:
range-base-for 默認是只讀遍歷,也就是說下邊的例子
這也許并不是我們想要的,如果要修改就要將遍歷的變量聲明為引用型。
也就是將代碼改成這樣:
for (auto& n :vec) std::cout << n++;2. 在遍歷容器的時候,auto自動推導的類型是容器的value_type類型,而不是迭代器。
之前提過range-based-for的實現方式,其中有范圍表達式是自動推導的
auto && __range = 范圍表達式 ;舉個例子,如果你遍歷的容器是map,那么value_type是std::pair,也就是說val的類型是std::pair類型的,因此需要使用val.first,val.second來訪問數據。
std::map<string, int> map = { { "a", 1 }, { "b", 2 }, { "c", 3 } }; for (auto &val : map) cout << val.first << "->" << val.second << endl;此外,使用基于范圍的for循環還要注意一些容器類本身的約束,比如set的容器內的元素本身有容器的特性就決定了其元素是只讀的,哪怕的使用了引用類型來遍歷set元素,也是不能修改器元素的,看下面例子:
set<int> ss = { 1, 2, 3, 4, 5, 6 }; for (auto& n : ss) cout << n++ << endl;3. 不能迭代的時候修改容器, 下面的代碼運行時候可能崩潰。
vector<int> vec = { 1, 2, 3, 4, 5, 6 }; int main(){ for (auto &n : vec) { cout << n << endl; vec.push_back(7); }}由于在遍歷容器的時候,在容器中插入一個元素導致迭代器失效了,因此,基于范圍的for循環和普通的for循環一樣,在遍歷的過程中如果修改容器,會造成迭代器失效。
4. 循環陷阱
說這個問題之前,還是要把它的實現再次粘貼過來(C++17以前):
{ auto && __range = 范圍表達式 ; for (auto __begin = 首表達式, __end = 尾表達式 ; __begin != __end; ++__begin) { 范圍聲明 = *__begin; 循環語句 }}舉個例子:
#include <iostream>#include <string> using namespace std; struct MyClass{ string text = "MyClass"; string& getText() { return text; }}; int main(){ for (auto ch : MyClass().text) { cout << ch; } cout << endl;}輸出結果就是
MyClass但是我們把代碼改成下邊的樣子, 結果什么都不會輸出,程序直接退出
for (auto ch : MyClass().getText()) { cout << ch; }為什么會出現這樣的現象呢?答案就在于range-based-for實現的時候有這樣一句話:
auto && __range = 范圍表達式 ;如果“范圍表達式”返回臨時量,則其生存期被延續到循環結尾,如綁定到轉發引用?__range?所示,要注意的是“范圍表達式”中任何臨時量生存期都不被延長。
for (auto& x : foo().items()) { /* .. */ } // 若 foo() 返回右值則為未定義行為原始的例子中,range_expression是 "MyClass().text",MyClass()是臨時對象,同時 "MyClass()" 這個表達式是右值。所以,"MyClass().text" 這個表達式也是右值,"MyClass().text" 這個對象是臨時對象中的一部分。所以,在 "auto && __range = range_expression;" 這個語句中,auto會被推導為 "std::string"。初始化右值引用為臨時對象的一部分時,可以延長整個臨時對象的生存期,在引用被銷毀時臨時對象才會被銷毀。所以for循環可以正常執行。
但是在修改過后,range_expression是 "MyClass().getText()"。同樣地,MyClass()是臨時對象,"MyClass()" 這個表達式是右值。但是 "getText()" 的返回類型為 "string&",所以,"MyClass().getText()" 這個表達式是左值。所以,在 "auto && __range = range_expression;" 這個語句中,auto會被推導為 "string &",語句等價于 "string & __range = range_expression;" 。雖然"MyClass().getText()" 這個對象是臨時對象中的一部分,但是在初始化非const的左值引用時,不會延長臨時對象的生存期,所以在這個初始化語句結束的同時MyClass()這個臨時對象就被銷毀了,__range成為了野引用,所以后面的循環語句可能會出現內存錯誤。
要解決這個問題,可以利用C++20 “初始化語句”變通解決:
for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK總結
以上是生活随笔為你收集整理的using(别名)和range based for的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 变长参数模板 和 外部模板
- 下一篇: 左值、右值、左值引用、右值引用