开发者都应该使用的10个C++11特性
auto
????在C++11之前,auto關鍵字用來指定存儲期。在新標準中,它的功能變為類型推斷。auto現在成了一個類型的占位符,通知編譯器去根據初始化代碼推斷所聲明變量的真實類型。各種作用域內聲明變量都可以用到它。例如,名空間中,程序塊中,或是for循環的初始化語句中。
auto?i?=?42;????????//?i?is?an?int auto?l?=?42LL;??????//?l?is?an?long?long auto?p?=?new?foo();?//?p?is?a?foo*使用auto通常意味著更短的代碼(除非你所用類型是int,它會比auto少一個字母)。試想一下當你遍歷STL容器時需要聲明的那些迭代器(iterator)。現在不需要去聲明那些typedef就可以得到簡潔的代碼了。
std::map<std::string,?std::vector<int>>?map; for(auto?it?=?begin(map);?it?!=?end(map);?++it)?{}需要注意的是,auto不能用來聲明函數的返回值。但如果函數有一個尾隨的返回類型時,auto是可以出現在函數聲明中返回值位置。這種情況下,auto并不是告訴編譯器去推斷返回類型,而是指引編譯器去函數的末端尋找返回值類型。在下面這個例子中,函數的返回值類型就是operator+操作符作用在T1、T2類型變量上的返回值類型。
template?<typename?T1,?typename?T2> auto?compose(T1?t1,?T2?t2)?->?decltype(t1?+?t2) {return?t1+t2; } auto?v?=?compose(2,?3.14);?//?v's?type?is?double?nullptr
????以前都是用0來表示空指針的,但由于0可以被隱式類型轉換為整形,這就會存在一些問題。關鍵字nullptr是std::nullptr_t類型的值,用來指代空指針。nullptr和任何指針類型以及類成員指針類型的空值之間可以發生隱式類型轉換,同樣也可以隱式轉換為bool型(取值為false)。但是不存在到整形的隱式類型轉換。
void?foo(int*?p)?{} void?bar(std::shared_ptr<int>?p)?{} int*?p1?=?NULL; int*?p2?=?nullptr;??? if(p1?==?p2)?{} foo(nullptr); bar(nullptr); bool?f?=?nullptr; int?i?=?nullptr;?//?error:?A?native?nullptr?can?only?be?converted?to?bool?or,?using?reinterpret_cast,?to?an?integral?type為了向前兼容,0仍然是個合法的空指針值。
Range-based for loops (基于范圍的for循環)
????為了在遍歷容器時支持”foreach”用法,C++11擴展了for語句的語法。用這個新的寫法,可以遍歷C類型的數組、初始化列表以及任何重載了非成員的begin()和end()函數的類型。如果你只是想對集合或數組的每個元素做一些操作,而不關心下標、迭代器位置或者元素個數,那么這種foreach的for循環將會非常有用。
std::map<std::string,?std::vector<int>>?map; std::vector<int>?v; v.push_back(1); v.push_back(2); v.push_back(3); map["one"]?=?v; for(const?auto&?kvp?:?map)? {std::cout?<<?kvp.first?<<?std::endl;for(auto?v?:?kvp.second){std::cout?<<?v?<<?std::endl;} } int?arr[]?=?{1,2,3,4,5}; for(int&?e?:?arr)? {e?=?e*e; }override和final
????我總覺得 C++中虛函數的設計很差勁,因為時至今日仍然沒有一個強制的機制來標識虛函數會在派生類里被改寫。vitual關鍵字是可選的,這使得閱讀代碼變得很費勁。因為可能需要追溯到繼承體系的源頭才能確定某個方法是否是虛函數。為了增加可讀性,我總是在派生類里也寫上virtual關鍵字,并且也鼓勵大家都這么做。即使這樣,仍然會產生一些微妙的錯誤。看下面這個例子:
class?A? { public:virtual?void?f(short)?{std::cout?<<?"A::f"?<<?std::endl;} }; class?B?:?public?A { public:virtual?void?f(int)?{std::cout?<<?"B::f"?<<?std::endl;} };B::f 按理應當重寫 A::f。然而二者的聲明是不同的,一個參數是short,另一個是int。因此B::f只是擁有同樣名字的另一個函數(重載)而不是重寫。當你通過A類型的指針調用f()可能會期望打印出B::f,但實際上則會打出 f(int)而不是f(short) 。另一個很微妙的錯誤情況:參數相同,但是基類的函數是const的,派生類的函數卻不是。
class?A? { public:virtual?void?f(int)?const?{std::cout?<<?"A::f?"?<<?std::endl;} }; class?B?:?public?A { public:virtual?void?f(int)?{std::cout?<<?"B::f"?<<?std::endl;} };同樣,這兩個函數是重載而不是重寫。幸運的是,現在有一種方式能描述你的意圖。新標準加入了兩個新的標識符(不是關鍵字)
override,表示函數必須重寫基類中的虛函數,如果派生類沒有重寫到將編譯報錯。
final,表示派生類不應當重寫這個虛函數,如果派生類重寫了基類的虛函數將編譯報錯。
class?A { public:virtual?void?f(short)?{?std::cout?<<?"A::f"?<<?std::endl;?}virtual?void?g(int)?final?{?std::cout?<<?"A::g"?<<?std::endl;?} }; class?B?:?public?A { public:virtual?void?f(short)??override??{?std::cout?<<?"B::f"?<<?std::endl;?}//virtual?void?g(int)?{?std::cout?<<?"A::g"?<<?std::endl;?}?//?error?C3248:?“main::A::g”:??聲明為“final”的函數無法被“main::B::g”重寫virtual?void?g(float)?{?std::cout?<<?"A::g"?<<?std::endl;?}?//?重載 };Strongly-typed enums 強類型枚舉
????傳統的C++枚舉類型存在一些缺陷:它們會將枚舉常量暴露在外層作用域中(這可能導致名字沖突,如果同一個作用域中存在兩個不同的枚舉類型,但是具有相同的枚舉常量就會沖突),而且它們會被隱式轉換為整形,無法擁有特定的用戶定義類型。
在C++11中通過引入了一個稱為強類型枚舉的新類型,修正了這種情況。強類型枚舉由關鍵字enum class標識。它不會將枚舉常量暴露到外層作用域中,也不會隱式轉換為整形,并且擁有用戶指定的特定類型(傳統枚舉也增加了這個性質)。
enum?class?Options?{None,?One,?All}; Options?o?=?Options::All;Smart Pointers 智能指針
????已經有成千上萬的文章討論這個問題了,所以我只想說:現在能使用的,帶引用計數,并且能自動釋放內存的智能指針包括以下幾種:
unique_ptr: 如果內存資源的所有權不需要共享,就應當使用這個(它沒有拷貝構造函數),但是它可以轉讓給另一個unique_ptr(存在move構造函數)。
shared_ptr: ?如果內存資源需要共享,那么使用這個(所以叫這個名字)。
weak_ptr: 持有被shared_ptr所管理對象的引用,但是不會改變引用計數值。它被用來打破依賴循環(想象在一個tree結構中,父節點通過一個共享所有權的引用(chared_ptr)引用子節點,同時子節點又必須持有父節點的引用。如果這第二個引用也共享所有權,就會導致一個循環,最終兩個節點內存都無法釋放)。
另一方面,auto_ptr已經被廢棄,不會再使用了。
什么時候使用unique_ptr,什么時候使用shared_ptr取決于對所有權的需求,我建議閱讀以下的討論:http://stackoverflow.com/questions/15648844/using-smart-pointers-for-class-members
std::unique_ptr<int>?p1(new?int(42)); std::unique_ptr<int>?p2?=?std::move(p1);?//?移交unique指針 auto?sp?=?std::make_shared<int>(42); std::weak_ptr<int>?wp?=?sp; {auto?sp?=?wp.lock();?//?提升shared_ptrstd::cout?<<?*sp?<<?std::endl; } sp.reset();if?(wp.expired())std::cout?<<?"expired"?<<?std::endl;如果你試圖鎖定(lock)一個過期(指被弱引用對象已經被釋放)的weak_ptr,那你將獲得一個空的shared_ptr.
Lambdas
????匿名函數(也叫lambda)已經加入到C++中,并很快異軍突起。這個從函數式編程中借來的強大特性,使很多其他特性以及類庫得以實現。你可以在任何使用函數對象或者函子(functor)或std::function的地方使用lambda。你可以從這里(http://msdn.microsoft.com/en-us/library/dd293603.aspx)找到語法說明。
std::vector<int>?v; v.push_back(1); v.push_back(2); v.push_back(3); std::for_each(std::begin(v),?std::end(v),?[](int?n)?{std::cout?<<?n?<<?std::endl;}); auto?is_odd?=?[](int?n)?{return?n%2==1;}; auto?pos?=?std::find_if(std::begin(v),?std::end(v),?is_odd); if(pos?!=?std::end(v))std::cout?<<?*pos?<<?std::endl;更復雜的是遞歸lambda。考慮一個實現Fibonacci函數的lambda。如果你試圖用auto來聲明,就會得到一個編譯錯誤。
auto?fib?=?[&fib](int?n)?{return?n?<?2???1?:?fib(n-1)?+?fib(n-2);}; error?C3533:?'auto?&':?a?parameter?cannot?have?a?type?that?contains?'auto' error?C3531:?'fib':?a?symbol?whose?type?contains?'auto'?must?have?an?initializer error?C3536:?'fib':?cannot?be?used?before?it?is?initialized error?C2064:?term?does?not?evaluate?to?a?function?taking?1?arguments問題出在auto意味著對象類型由初始表達式決定,然而初始表達式又包含了對其自身的引用,因此要求先知道它的類型,這就導致了無窮遞歸。解決問題的關鍵就是打破這種循環依賴,用std::function顯式的指定函數類型:
std::function<int(int)>?lfib?=?[&lfib](int?n)?{return?n?<?2???1?:?lfib(n-1)?+?lfib(n-2);};?非成員begin()和end()
????也許你注意到了,我在前面的例子中已經用到了非成員begin()和end()函數。他們是新加入標準庫的,除了能提高了代碼一致性,還有助于更多地使用泛型編程。它們和所有的STL容器兼容。更重要的是,他們是可重載的。所以它們可以被擴展到支持任何類型。對C類型數組的重載已經包含在標準庫中了。我們還用上一個例子中的代碼來說明,在這個例子中我打印了一個數組然后查找它的第一個偶數元素。如果std::vector被替換成C類型數組。代碼可能看起來是這樣的:
int?arr[]?=?{1,2,3}; std::for_each(&arr[0],?&arr[0]+sizeof(arr)/sizeof(arr[0]),?[](int?n)?{std::cout?<<?n?<<?std::endl;}); auto?is_odd?=?[](int?n)?{return?n%2==1;}; auto?begin?=?&arr[0]; auto?end?=?&arr[0]+sizeof(arr)/sizeof(arr[0]); auto?pos?=?std::find_if(begin,?end,?is_odd); if(pos?!=?end) ????std::cout?<<?*pos?<<?std::endl;如果使用非成員的begin()和end()來實現,就會是以下這樣的:
int?arr[]?=?{1,2,3}; std::for_each(std::begin(arr),?std::end(arr),?[](int?n)?{std::cout?<<?n?<<?std::endl;}); auto?is_odd?=?[](int?n)?{return?n%2==1;}; auto?pos?=?std::find_if(std::begin(arr),?std::end(arr),?is_odd); if(pos?!=?std::end(arr))std::cout?<<?*pos?<<?std::endl;這基本上和使用std::vecto的代碼是完全一樣的。這就意味著我們可以寫一個泛型函數處理所有支持begin()和end()的類型。
template?<typename?Iterator> void?bar(Iterator?begin,?Iterator?end) {std::for_each(begin,?end,?[](int?n)?{std::cout?<<?n?<<?std::endl;});auto?is_odd?=?[](int?n)?{return?n%2==1;};auto?pos?=?std::find_if(begin,?end,?is_odd);if(pos?!=?end)std::cout?<<?*pos?<<?std::endl; } template?<typename?C> void?foo(C?c) {bar(std::begin(c),?std::end(c)); } template?<typename?T,?size_t?N> void?foo(T(&arr)[N]) {bar(std::begin(arr),?std::end(arr)); } int?arr[]?=?{1,2,3}; foo(arr); std::vector<int>?v; v.push_back(1); v.push_back(2); v.push_back(3); foo(v);static_assert和 type traits
????static_assert提供一個編譯時的斷言檢查。如果斷言為真,什么也不會發生。如果斷言為假,編譯器會打印一個特殊的錯誤信息。
template?<typename?T,?size_t?Size> class?Vector {static_assert(Size?<?3,?"Size?is?too?small");T?_points[Size]; }; int?main() {Vector<int,?16>?a1;Vector<double,?2>?a2;return?0; } error?C2338:?Size?is?too?small see?reference?to?class?template?instantiation?'Vector<T,Size>'?being?compiledwith[T=double,Size=2]static_assert和type traits一起使用能發揮更大的威力。type traits是一些class,在編譯時提供關于類型的信息。在頭文件<type_traits>中可以找到它們。這個頭文件中有好幾種class: helper class,用來產生編譯時常量。type traits class,用來在編譯時獲取類型信息,還有就是type transformation class,他們可以將已存在的類型變換為新的類型。
下面這段代碼原本期望只做用于整數類型。
但是如果有人寫出如下代碼,編譯器并不會報錯
std::cout?<<?add(1,?3.14)?<<?std::endl; std::cout?<<?add("one",?2)?<<?std::endl;程序會打印出4.14和”e”。但是如果我們加上編譯時斷言,那么以上兩行將產生編譯錯誤。
template?<typename?T1,?typename?T2> auto?add(T1?t1,?T2?t2) {static_assert(std::is_integral<T1>::value,?"Type?T1?must?be?integral");static_assert(std::is_integral<T2>::value,?"Type?T2?must?be?integral");return?t1?+?t2; } error?C2338:?Type?T2?must?be?integral see?reference?to?function?template?instantiation?'T2?add<int,double>(T1,T2)'?being?compiledwith[T2=double,T1=int] error?C2338:?Type?T1?must?be?integral see?reference?to?function?template?instantiation?'T1?add<const?char*,int>(T1,T2)'?being?compiledwith[T1=const?char?*,T2=int]Move semantics (Move語義)
????這是C++11中所涵蓋的另一個重要話題。就這個話題可以寫出一系列文章,僅用一個段落來說明顯然是不夠的。因此在這里我不會過多的深入細節,如果你還不是很熟悉這個話題,我鼓勵你去地資料。C++11加入了右值引用(value reference)的概念(用&&標識),用來區分對左值和右值的引用。左值就是一個有名字的對象,而右值則是一個無名對象(臨時對象)。move語義允許修改右值(以前右值被看作是不可修改的,等同于const T&類型)。C++的class或者struct以前都有一些隱含的成員函數:默認構造函數(僅當沒有顯示定義任何其他構造函數時才存在),拷貝構造函數,析構函數還有拷貝賦值操作符。拷貝構造函數和拷貝賦值操作符提供bit-wise的拷貝(淺拷貝),也就是逐個bit拷貝對象。也就是說,如果你有一個類包含指向其他對象的指針,拷貝時只會拷貝指針的值而不會管指向的對象。在某些情況下這種做法是沒問題的,但在很多情況下,實際上你需要的是深拷貝,也就是說你希望拷貝指針所指向的對象。而不是拷貝指針的值。這種情況下,你需要顯示地提供拷貝構造函數與拷貝賦值操作符來進行深拷貝。如果你用來初始化或拷貝的源對象是個右值(臨時對象)會怎么樣呢?你仍然需要拷貝它的值,但隨后很快右值就會被釋放。這意味著產生了額外的操作開銷,包括原本并不需要的空間分配以及內存拷貝。現在說說move constructor和move assignment operator。這兩個函數接收T&&類型的參數,也就是一個右值。在這種情況下,它們可以修改右值對象,例如“偷走”它們內部指針所指向的對象。舉個例子,一個容器的實現(例如vector或者queue)可能包含一個指向元素數組的指針。當用一個臨時對象初始化一個對象時,我們不需要分配另一個數組,從臨時對象中把值復制過來,然后在臨時對象析構時釋放它的內存。我們只需要將指向數組內存的指針值復制過來,由此節約了一次內存分配,一次元數組的復制以及后來的內存釋放。以下代碼實現了一個簡易的buffer。這個buffer有一個成員記錄buffer名稱(為了便于以下的說明),一個指針(封裝在unique_ptr中)指向元素為T類型的數組,還有一個記錄數組長度的變量。
template?<typename?T> class?Buffer? {std::string??????????_name;size_t???????????????_size;std::unique_ptr<T[]>?_buffer;public://?default?constructorBuffer():_size(16),_buffer(new?T[16]){}//?constructorBuffer(const?std::string&?name,?size_t?size):_name(name),_size(size),_buffer(new?T[size]){}//?copy?constructorBuffer(const?Buffer&?copy):_name(copy._name),_size(copy._size),_buffer(new?T[copy._size]){T*?source?=?copy._buffer.get();T*?dest?=?_buffer.get();std::copy(source,?source?+?copy._size,?dest);}//?copy?assignment?operatorBuffer&?operator=(const?Buffer&?copy){if(this?!=??){_name?=?copy._name;if(_size?!=?copy._size){_buffer?=?nullptr;_size?=?copy._size;_buffer?=?_size?>?0?>?new?T[_size]?:?nullptr;}T*?source?=?copy._buffer.get();T*?dest?=?_buffer.get();std::copy(source,?source?+?copy._size,?dest);}return?*this;}//?move?constructorBuffer(Buffer&&?temp):_name(std::move(temp._name)),_size(temp._size),_buffer(std::move(temp._buffer)){temp._buffer?=?nullptr;temp._size?=?0;}//?move?assignment?operatorBuffer&?operator=(Buffer&&?temp){assert(this?!=?&temp);?//?assert?if?this?is?not?a?temporary_buffer?=?nullptr;_size?=?temp._size;_buffer?=?std::move(temp._buffer);_name?=?std::move(temp._name);temp._buffer?=?nullptr;temp._size?=?0;return?*this;} };template?<typename?T> Buffer<T>?getBuffer(const?std::string&?name)? {Buffer<T>?b(name,?128);return?b; } int?main() {Buffer<int>?b1;Buffer<int>?b2("buf2",?64);Buffer<int>?b3?=?b2;Buffer<int>?b4?=?getBuffer<int>("buf4");b1?=?getBuffer<int>("buf5");return?0; }默認的copy constructor以及copy assignment operator大家應該很熟悉了。C++11中新增的是move constructor以及move assignment operator,這兩個函數根據上文所描述的move語義實現。如果你運行這段代碼,你就會發現b4構造時,move constructor會被調用。同樣,對b1賦值時,move assignment operator會被調用。原因就在于getBuffer()的返回值是一個臨時對象——也就是右值。你也許注意到了,move constuctor中當我們初始化變量name和指向buffer的指針時,我們使用了std::move。name實際上是一個string,std::string實現了move語義。std::unique_ptr也一樣。但是如果我們寫_name(temp._name),那么copy constructor將會被調用。不過對于_buffer來說不能這么寫,因為std::unique_ptr沒有copy constructor。但為什么std::string的move constructor此時沒有被調到呢?這是因為雖然我們使用一個右值調用了Buffer的move constructor,但在這個構造函數內,它實際上是個左值。為什么?因為它是有名字的——“temp”。一個有名字的對象就是左值。為了再把它變為右值(以便調用move constructor)必須使用std::move。這個函數僅僅是把一個左值引用變為一個右值引用。更新:雖然這個例子是為了說明如何實現move constructor以及move assignment operator,但具體的實現方式并不是唯一的。某同學提供了另一種可能的實現。為了方便查看,我把它也列在下面:
template?<typename?T> class?Buffer {std::string??????????_name;size_t???????????????_size;std::unique_ptr<T[]>?_buffer;public://?constructorBuffer(const?std::string&?name?=?"",?size_t?size?=?16):_name(name),_size(size),_buffer(size??new?T[size]?:?nullptr){}//?copy?constructorBuffer(const?Buffer&?copy):_name(copy._name),_size(copy._size),_buffer(copy._size??new?T[copy._size]?:?nullptr){T*?source?=?copy._buffer.get();T*?dest?=?_buffer.get();std::copy(source,?source?+?copy._size,?dest);}//?copy?assignment?operatorBuffer&?operator=(Buffer?copy){swap(*this,?copy);return?*this;}//?move?constructorBuffer(Buffer&&?temp):Buffer(){swap(*this,?temp);}friend?void?swap(Buffer&?first,?Buffer&?second)?noexcept{using?std::swap;swap(first._name??,?second._name);swap(first._size??,?second._size);swap(first._buffer,?second._buffer);} };?結論
????關于C++11還有很多要說的。本文只是各種入門介紹中的一個。本文展示了一系列C++開發者應當使用的核心語言特性與標準庫函數。然而我建議你能更加深入地學習,至少也要再看看本文所介紹的特性中的部分。
總結
以上是生活随笔為你收集整理的开发者都应该使用的10个C++11特性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 双目标定与矫正 matlab
- 下一篇: 双目立体匹配算法