現代C++語言(C++11/14/17)特性總結和使用建議(三)

17.png

noexcept修飾符與noexcept操作符

noexcept形如起名,表示其修飾的函數不會拋出異常(在C++11中如果noexcept修飾的函數拋出了異常,編譯器可以選擇直接調用std::terminate()函數來終止程序的運行),有2種語法形式:

一種就是簡單的在函數聲明後加上noexcept關鍵字,比如:

1.png

另外一種則可以接受一個常量表達式(結果會被轉換成一個bool類型的值,true表示不會拋出異常,反之有可能拋出異常)作爲參數,如下所示:

2.png

而noexcept作爲一個操作符時,通常可以用於模板。比如:

 

3.png

以前的C++代碼中,異常特性就很少被使用,因此noexcept需要被用到的場景也很少。但是有一個地方強烈建議使用:自定義類型的移動構造函數和移動賦值操作符。因爲部分STL容器會判斷:如果元素的移動構造函數和移動賦值操作符並非noexcept,將不會調用,而是繼續用老的複製對象方式,導致性能上的損失。

相關鏈接:https://zh.cppreference.com/w/cpp/language/noexcept_spec

decltype關鍵字

decltype實際上有點像auto的反函數,auto可以讓你聲明一個變量,而decltype則可以從一個變量或表達式中得到類型。

4.png

一般的代碼很少需要用到decltype,但是也有不得不用的場景:若要獲取一個lamda表達式的類型,由於其類型不存在名字,唯一的方法只能是用decltype。另外在一些模板場景下也有用處。

相關鏈接:https://zh.cppreference.com/w/cpp/language/decltype

static_assert關鍵字

static_assert提供一個編譯時的斷言檢查。如果斷言爲真,什麼也不會發生。如果斷言爲假,編譯器會打印一個特殊的錯誤信息。

相對於以前的運行時檢查assert,static_assert更不容易被誤用,也沒有什麼安全風險(因爲運行時什麼也不會幹)。建議所有在編譯期能夠檢查出來的錯誤都用static_assert來檢查。

相關鏈接:https://zh.cppreference.com/w/cpp/language/static_assert

變長參數的模板

以前的C/C++允許函數參數爲變長,但是可選參數都是沒有類型檢查的,這導致了一大類運行時問題——比如把自定義結構體傳給printf打印。現在,C++允許模板參數也爲變長,和函數不同的是,每個模板參數都是有類型檢查的。

一個使用變長參數模板的例子是Print函數,在C語言中printf可以傳入多個參數,在C++11中,我們可以用變長參數模板實現更簡潔的Print:

5.png

變長參數的模板在普通場景下較少需要用到。對於打印輸出來說,更好的做法是徹底棄用printf這類函數而改用std::ostream。另外,對於二進制文件大小敏感的場景,需要注意變長參數的模板會帶來模板膨脹問題——任一個參數不同都會生成一個新的函數/類型實例。

相關鏈接:https://zh.cppreference.com/w/cpp/language/parameter_pack

繼承構造函數和委託構造函數

繼承構造函數允許子類聲明一個和父類一樣的構造函數;委託構造函數允許不同參數類型的構造函數之間進行復用。這兩個特性需要被使用的場景並不多見,以前程序員普遍習慣了通過父類構造函數顯式調用和缺省參數來達到同樣的效果,很難說新特性寫法的可讀性是否更好。

相關鏈接:https://zh.cppreference.com/w/cpp/language/initializer_list

顯式轉換操作符

顯式轉換操作符限制自定義的類型轉換操作符不能用於隱式轉換,通常情況下應用場景有限。

相關鏈接:https://zh.cppreference.com/w/cpp/language/explicit

非靜態數據成員的類定義內初始化

C++11允許直接在類定義內來初始化成員變量。

6.png

相比起原來只能在構造函數中初始化的方法,直接在類定義中給出初始化值的方法有着更好的可讀性,值得推廣。

相關鏈接:https://zh.cppreference.com/w/cpp/language/data_members

原生字符串字面量

原生字符串字面量(raw string literal)使得用戶書寫的字符串“所見即所得”,不需要如'\t'、'\n'等控制字符來調整字符串中的格式。C++11中引入了原生字符串字面量的支持,聲明相當簡潔,程序員只需要在字符串前加入前綴,即字母R,並在引號中使用括號左右標識,就可以聲明該字符串字面量爲原生字符串了。

7.png

而對於Unicode的字符串,也可以通過相同的方式聲明。聲明UTF-8、UTF-16、UTF-32的原生字符串字面量,將其前綴分別設爲u8R、uR、UR就可以了。不過有一點需要注意,使用了原生字符串的話,轉義字符就不能再使用了,這會給想使用\u或者\U的方式寫Unicode字符的程序員帶來一定影響。

此外,原生字符串字面量也像C的字符串字面量一樣遵從連接規則。

原生字符串在一些需要大段字符串常量的場景下很有用,比如當測試代碼中要寫一個很長的JSON字符串,用原生字符串就會比原來方便很多。

相關鏈接:https://en.cppreference.com/w/cpp/language/string_literal

C++11新特性(標準庫)

unordered_set和unordered_map

C++11中出現了兩種新的關聯容器:unordered_set和unordered_map,其內部實現與set和map大有不同,set和map內部實現是基於RB-Tree,而unordered_set和unordered_map內部實現是基於哈希表(hashtable),unordered_set和unordered_map內部實現的公共接口大致相同。

unordered_set和unordered_map是基於哈希表,因此要了解unordered_set/unordered_map,就必須瞭解哈希表的機制。哈希表是根據關鍵碼值而進行直接訪問的數據結構,通過相應的哈希函數(也稱散列函數)處理關鍵字得到相應的關鍵碼值,關鍵碼值對應着一個特定位置,用該位置來存取相應的信息,這樣就能以較快的速度獲取關鍵字的信息。面對哈希衝突時,unordered_set/unordered_map內部解決衝突採用的是——鏈地址法,當用衝突發生時把具有同一關鍵碼的數據組成一個鏈表。

在一個unordered_set/unordered_map內部,元素不會按任何順序排序,而是通過元素值的hash值將元素分組放置到各個槽(Bucker,也可以譯爲“桶”),這樣就能通過元素值快速訪問各個對應的元素(均攤耗時爲O(1))。

8.png

C++標準庫中沒有提供hash表容器一直是一個被廣爲詬病的問題。hash表在當今的軟件系統中有着非常廣泛的用途,很多項目組都需要它來達到最好的查找性能。過去,大多數程序員通過set、map來達成類似的效果,但實際上它們的查找性能和hash表相比有着顯著的差距。從C++11開始,hash表正式進入了標準庫,各個項目也應當大力推廣使用。

雖然unordered_set和unordered_map具有優秀的性能,但是對於用戶自定義類型作爲key的場景,如何實現恰當的hash函數是個不可忽視的技術活。對於set/map這種二叉樹結構來說,用戶自定義類型只要按固定範式來實現operator<就可以了。但hash函數可遠不像operator<這麼好實現。有些程序員覺得只要把所有成員的hash值加起來就可以了,這是種幼稚的實現,這種方法產生的hash容器性能會很差。

關於hash函數如何實現的討論,可以參考這個頁面:https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key

鑑於hash函數的實現太過於考驗程序員的功力,建議各項目組在使用hash表時,由項目統一提供輔助hash函數工具,不要讓每個程序員自己去實現。

相關鏈接:https://zh.cppreference.com/w/cpp/container/unordered_set
https://zh.cppreference.com/w/cpp/container/unordered_map

std::tuple

類模板std::tuple是固定大小的異類值彙集,它是std::pair的推廣。

9.png

在沒有std::tuple時,要表示一個聚合就只能自定義一個結構體(或類)。std::tuple提供了另一種選擇,可以在不額外定義類型的情況下表示任意幾種類型的聚合。當然,在什麼場景下用哪種方案更好還需要程序員自己決定。

相關鏈接:https://zh.cppreference.com/w/cpp/utility/tuple

std::tie

std::tie通常結合std::pair或std::tuple來使用。以前,C++一個函數要返回多個值,或者將多個值一對一的賦值/比較都是很麻煩的事,std::tie可以很有效的簡化代碼。

10.png

建議在新代碼中,遇到自定義類型重載<運算符,以及臨時解析std::pair、std::tuple內容時,都用std::tie來簡化代碼。

相關鏈接:https://zh.cppreference.com/w/cpp/utility/tuple/tie

std::array

std::array 是封裝固定大小數組的容器。此容器是一個聚合類型,其語義等同於保有一個C風格數組T[N]作爲其唯一非靜態數據成員的結構體。不同於 C 風格數組,它不會自動退化成 T* 。

11.png

std::array是個值得大力推廣用來替代數組的特性。相比於數組,其at成員函數帶有越界檢查,可以杜絕一大類數組越界隱患(但是要注意真的越界後產生的std::out_of_range異常的處理方式)。另外,std::array不能自動轉換成指針的特點也讓它避免了被開發人員濫用,並杜絕了數組形參退化成指針導致的一大類常見問題。back、fill、swap等成員函數比起手寫的數組操作也更加簡潔。

相關鏈接:https://zh.cppreference.com/w/cpp/container/array

std::bind

很多算法接口要求提供一個函數對象,但程序員手頭有可能只有一個參數並不匹配的現成函數,這時可以通過函數適配器或者lamda表達式來進行兩個接口之間的適配。在過去,C++提供了std::bind1st,std::bind2st等函數來指定綁定哪一個參數。但顯然這些工具用起來太麻煩。C++11廢除了以前這些適配器,提供了一個統一的bind函數模板,可以自動的根據傳入參數類型和個數決定如何來適配。

12.png

std::bind相對於lamda表達式孰優孰劣就很難說了。如果函數的參數過多或重排序很複雜,_1、_2、_3這一堆佔位符寫在代碼裏恐怕算不上可讀性多好。因此,建議最好只在參數綁定較簡單的場景下使用std::bind。

相關鏈接:https://zh.cppreference.com/w/cpp/utility/functional/bind

Smart Pointers(智能指針):unique_ptr、shared_ptr、weak_ptr

現在能使用的,帶引用計數,並且能自動釋放內存的智能指針包括以下幾種:

unique_ptr: 如果內存資源的所有權不需要共享,就應當使用這個(它沒有拷貝構造函數),但是它可以轉讓給另一個unique_ptr(存在move構造函數)。
shared_ptr: 如果內存資源需要共享,那麼使用這個(所以叫這個名字)。
weak_ptr: 持有被shared_ptr所管理對象的引用,但是不會改變引用計數值。它被用來打破依賴循環(想象在一個tree結構中,父節點通過一個共享所有權的引用(shared_ptr)引用子節點,同時子節點又必須持有父節點的引用。如果這第二個引用也共享所有權,就會導致一個循環,最終兩個節點內存都無法釋放)。
另一方面,auto_ptr已經被廢棄,不會再使用了。

什麼時候使用unique_ptr,什麼時候使用shared_ptr取決於對所有權的需求,建議閱讀以下的討論:
http://stackoverflow.com/questions/15648844/using-smart-pointers-for-class-members

以下第一個例子使用了unique_ptr。如果你想把對象所有權轉移給另一個unique_ptr,需要使用std::move。在所有權轉移後,交出所有權的智能指針將爲空,get()函數將返回nullptr。

13.png

第二個例子展示了shared_ptr。用法相似,但語義不同,此時所有權是共享的。

14.png

第一個聲明和以下這行是等價的:

25.png

make_shared<T>是一個非成員函數,使用它的好處是可以一次性分配共享對象和智能指針自身的內存。而顯式的使用shared_ptr構造函數來構造則至少需要兩次內存分配。除了會產生額外的開銷,還可能會導致內存泄漏。在下面這個例子中,如果seed()拋出一個錯誤就會產生內存泄漏。

15.png

如果使用make_shared就不會有這個問題了。第三個例子展示了weak_ptr。注意,你必須調用lock()來獲得被引用對象的shared_ptr,通過它才能訪問這個對象。

16.png

如果你試圖鎖定(lock)一個過期(指被弱引用對象已經被釋放)的weak_ptr,那你將獲得一個空的shared_ptr。

智能指針可以在一定程度上對防範內存問題有幫助,但並不是萬能的。智能指針如果使用不當,仍可能出現內存泄露、重複釋放等各種問題。對於需要限制對象生命週期在某個局部作用域的場景,unique_ptr是個很好的選擇,以前的auto_ptr應當被廢棄。而對於對象生命週期超出局部作用域的場景來說,在設計上限定“誰申請,誰釋放”很可能是比shared_ptr更好的解決方案。因爲即使用了shared_ptr,也很難預知對象什麼時候被釋放,是否所有使用的代碼都嚴格按規則使用,還是需要大量人工檢查。而且,如果shared_ptr出了問題,有可能比以前更難定位。

相關鏈接:https://zh.cppreference.com/w/cpp/memory/unique_ptr
https://zh.cppreference.com/w/cpp/memory/shared_ptr
https://zh.cppreference.com/w/cpp/memory/weak_ptr

C++14新特性(語言核心)

返回類型推導

C++11允許lambda函數根據return語句的表達式類型推斷返回類型,C++14爲一般的函數也提供了這個能力。C++14還拓展了原有的規則,使得函數體並不是{return expression;}形式的函數也可以使用返回類型推導。

爲了啓用返回類型推導,函數聲明必須將auto作爲返回類型,但沒有C++11的後置返回類型說明符:

 

如果函數實現中含有多個return語句,這些表達式必須可以推斷爲相同的類型。

使用返回類型推導的函數可以前向聲明,但在定義之前不可以使用。它們的定義在使用它們的翻譯單元(translation unit)之中必須是可用的。

這樣的函數中可以存在遞歸,但遞歸調用必須在函數定義中的至少一個return語句之後:

18.png

返回類型推導雖然方便,但對於一般的函數來說爲了可讀性不建議隨意使用。而在模板編程中,有些場景下使用返回類型推導可以帶來很大的方便。

相關鏈接:https://zh.cppreference.com/w/cpp/language/auto

decltype(auto)

C++11中有兩種推斷類型的方式。auto根據給出的表達式產生具有合適類型的變量。decltype可以計算給出的表達式的類型。但是,decltype和auto推斷類型的方式是不同的。特別地,auto總是推斷出非引用類型,就好像使用了std::remove_reference一樣,而auto&&總是推斷出引用類型。然而decltype可以根據表達式的值類別和表達式的性質推斷出引用或非引用類型:

19.png

C++14增加了decltype(auto)的語法。允許auto的類型聲明使用decltype的規則。也即,允許不必顯式指定作爲decltype參數的表達式,而使用decltype對於給定表達式的推斷規則。

decltype(auto)的語法也可以用於返回類型推導,只需用decltype(auto)代替auto。

20.png

decltype(auto)可以解決特定場景下的問題,在一般場景下用auto就足夠了。

相關鏈接:https://zh.cppreference.com/w/cpp/language/auto

放開constexpr限制

在C++11之後,編譯期的數值計算可以通過使用constexpr聲明並定義編譯期函數來進行。相對於模板元編程,使用constexpr函數更貼近普通的C++程序,計算過程顯得更爲直接,意圖也更明顯。但在C++11中constexpr函數所受到的限制較多,比如函數體通常只有一句return語句,函數體內既不能聲明變量,也不能使用for語句之類的常規控制流語句。

C++14解除了對constexpr函數的大部分限制。在C++14的constexpr函數體內我們既可以聲明變量,也可以使用goto和try之外大部分的控制流語句。

雖說constexpr函數所定義的是編譯期的函數,但實際上在運行期constexpr函數也能被調用。事實上,如果使用編譯期常量參數調用constexpr函數,我們就能夠在編譯期得到運算結果;而如果使用運行期變量參數調用constexpr函數,那麼在運行期我們同樣也能得到運算結果。

準確的說,constexpr函數是一種在編譯期和運行期都能被調用並執行的函數。出於constexpr函數的這個特點,在C++11之後進行數值計算時,無論在編譯期還是運行期我們都可以統一用一套代碼來實現。編譯期和運行期在數值計算這點上得到了部分統一。

21.png

相關鏈接:https://zh.cppreference.com/w/cpp/language/constexpr

變量模板

變量模板是C++14的一個新的語法特性。C++新標準引入變量模板的主要目的是爲了簡化定義(simplify definitions)以及對模板化常量(parameterized constant)的支持

22.png

變量模板是個較爲生僻的特性,在一般的場景下較少會用到。

相關鏈接:https://zh.cppreference.com/w/cpp/language/variable_template

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章