這篇文章記錄了學習侯捷老師C++11/14課程的筆記。C++11是C++2.0,引入了許多新的特性,將從語言層面和標准庫層面來介紹這些新的特性。
由於直接引用的github倉庫的圖片,可能會有圖片顯示問題,可以在https://github.com/FangYang970206/Cpp-Notes/releases下載完整pdf版本的筆記,轉載請注明地址,謝謝~
語言層面
怎樣確定C++環境是否支持C++11呢?
使用如下語句:
cout << __cplusplus << endl;
如果出現的頭六位數是大於等於201103的,則支持C++11。
模板表達式中的空格
在C++11之前,模板后面的尖括號需要空格,C++11之后就不需要了。
nullptr和std::nullptr_t
使用nullptr代替NULL和0來代表指針沒有指向值。這可以避免把空指針當int而引發錯誤。上圖中給出了函數調用的實例,使用nullptr不會出現這種問題,這是因為nullptr是std::nullptr_t類型,c++11以后,std::nullptr_t也是基礎類型了,可以自己定義變量。
自動推導類型----auto
C++11之后,可以使用auto自動推導變量和對象的類型,而不需要自己手寫出來,對於有非常復雜而長的類型來說,這是很方便的,另外auto還可以自動推導lambda表達式的類型,然后就可以把lambda函數當普通函數使用。
典型用法(更簡單):
標准庫中的使用:
一致性初始化----Uniform Initialization
C++11引入了一個通用的初始化方式——一致性初始化,使用大括號括起來進行初始化,編譯器看到這種初始化會轉換成一個initializer_list
- 如果對象帶有接受initializer_list
的構造函數版本,那使用該構造函數進行初始化。(如上vector 初始化) - 如果對象沒有initializer_list
的構造函數版本,那編譯器會將initializer_list 逐一分解,傳給對應的構造函數。(如上complex初始化)
另外,如果函數的參數就是initializer_list
初始化列表(initializer_list)
大括號可以設定初值(默認值),另外,大括號初始化不允許窄化轉換(書籍上這樣說的,實際gcc只會給出警告,但這不是好習慣)。
C++11提供了一個std::initializer_list<>, 可以接受任意個數的相同類型,上面是一個實例。
上圖的左邊是initializer_list在構造函數中的應用,右邊是initializer_list的源代碼,它的內部有一個array和一個長度,另外initializer_list的構造函數是私有的,但編譯器當看到大括號的時候,就會調用這個構造函數,編譯器有無上權力。initializer_list構造函數會傳入array(C++11新提出的,對數組進行封裝,可以使用算法庫)的頭部迭代器,以及它的長度。
上面是array的源代碼,里面就是一個基本的數組,然后封裝了begin和end迭代器。
左下角是新東西,其他的之前出現過了,第一句話是指initializer_list背后有一個array在支撐,第二句話是說initializer_list並沒有包含那個array,只是有一個指針指向那個array的頭部和一個size_t等於array的長度。如果拷貝initializer_list,只是淺拷貝,指向同一個array以及得到同一個長度。最后一句話是說那個臨時array的生命周期與initializer_list是相同的。
這是標准庫中使用initializer_list的各個地方,非常之多,這里只列舉vector里面的使用,有初始化,重載賦值運算符,插入以及分配。
上面的是具體事例以及對應調用有initializr_list的方法。
explicit
這節在C++面向對象高級編程中有很多的補充,可以去看看,在構造函數前面加上explicit, 就是告訴編譯器, 不要在將int類型隱式轉成Complex, 只能通過顯式地進行構造。
之前一張圖是只有一個實參,這里是多個實參的例子,當使用運行p3{77, 5, 42}的時候,直接調用的是帶有initializer_list的構造函數(一致性初始化),而p5 = {77, 5, 42}, {77, 5, 42}是initialization_list
range-based for
這一小節講的是非常實用的for,C++11提供了range-based for,如上所述,decl是申明,coll是容器,意思是一個個拿出coll中的元素,下面有實例,可以搭配auto使用,非常方便,需要in-place的話,加上&即可。
左邊是range-based for,右邊是編譯器對它的解釋。
這是explicit的一個例子,禁止編譯器隱式將String轉化C,所以會報錯。
=default, =delete
=default要的是編譯器給的default ctor,=delete是不要對應的ctor,例如,上述的Zoo(const Zoo&)=delete
是說不要拷貝構造,Zoo(const Zoo&&)=default
是說要編譯器默認給我的那一個。
以上三張圖是C++標准庫中使用=default和=delete的事例,標准庫都用了,那自然是好的。(這里注意析構函數不能用=delete,可能會出大問題)
構造函數可以有多個版本,上述定義了兩個Foo的構造函數,一個是有實參的,另一個使用=default得到編譯器默認給出的構造函數。對於拷貝構造而言,只能允許一個,所以當使用=default的時候,由於已經寫出一個了,就無法進行重載了,而使用=delete的時候,由於寫出來了,無法進行刪除了。拷貝賦值情況類似。對於一般函數來說,沒有default版本,所以對一般函數進行=default是不對的,但=delete可以有,但沒必要,寫出來不要還不如不寫。上圖中還給出了=default,=delete與=0的區別,區別在與=default只能用於big-five(構造函數,拷貝構造,賦值構造,析構,移動構造,移動賦值), =delete可以用於任何函數,但有時沒有必要使用,如上面所說,而=0只能用於虛函數,代表純虛函數。
對於一個空的class,C++會在空的class內部插入一些代碼(默認的構造函數,拷貝構造,拷貝賦值以及析構函數,都是public並且是inline的),這樣才會使左下角的的代碼運行正常,作用還不止這些,這些默認的函數還給編譯器放置藏身幕后的一些代碼,比如當涉及繼承的時候,調用base classes的構造和析構就會對應放置在默認生成的構造和析構當中。
如果一個類帶有pointer member,則需要自己定義big-three,而沒有pointer member的話,用編譯器默認提供的就足夠了。上面的complex就是直接使用編譯器默認提供的拷貝賦值和析構。更詳細的推薦看我寫的面向對象程序設計_part1部分的筆記,有非常詳細的講述。
上圖是=default和=delete的使用事例,class NoCopy把拷貝構造和拷貝賦值都=delete,也就是沒有這兩個了,不允許外界去拷貝這個類的對象,這個在一些事例上是有用的。class NoDtor則不要析構函數了,對象創建無法刪除,會報錯。(一般不會這么使用)最后的PrivateCopy把拷貝構造和拷貝賦值放入了private里面,這限制了訪問這兩個函數的使用者,一般用戶代碼無法調用,但友元以及成員可以進行拷貝。
這是一個Boost庫的例子,與上述的PrivateCopy一樣,它的作用是讓其他類繼承這個類,這樣其他類也擁有noncopyable同樣的性質。
Alias Template 與 Template Template parameter
C++11引入了Alias Template,用法如上所示,先些template
Alias template難道只是少打幾個字嗎? 不是的,上圖進行說明,函數test_moveable測試不同容器的move操作(右值引用)和拷貝操作的時間比較,想使用容器和元素的類型,這是天方夜談的,container和T是不能再函數內部使用,報出了三個錯誤。然后再進行改進,改成右邊形式的,利用函數模板的實參推導可以推出Container和T的類型,不然依然是天方夜譚,編譯器不認識Container是個模板,無法使用尖括號
這一頁在Container前面加上了typename,告訴編譯器Container
上圖就是解決方案,傳入的實參只有一個,根據模板函數的自動推導,得到它的迭代器(前面要加typename),然后通過一個迭代器萃取機引出對象的Value_type, 然后根據typedef得到值類型,這樣就不會報錯了。然后看右上角黃色的話語,如果沒有iterator和traits,該怎么解決這一問題呢?上面就是思考路徑,在模板接受模板,能不能從中取出模板的參數?
這就需要template template parameter了。
模板模板參數是模板嵌套模板,如上面所示,XCI接受兩個參數,第一個是T,第二個是模板Container,然后就可以直接使用Container<T> c;
因為Container是一個模板,但再調用XCIs<Mystring, vector> c1;
的時候,出現報錯,原因是vector有兩個模板參數,第二個模板參數(分配器)是默認的,但編譯器不知道,這個時候就需要用到Alias Template了。
使用Alias Template,就可以將Vec變為一個模板參數的模板,然后就可以初始化對象了。可以看到Alias Template不僅是少打幾個字,還有減少模板參數個數以適配模板模板參數,非常有用處。
Type Alias
Type Alias是另一個typedef的寫法,不過更加清晰,通過using關鍵字去實現,左上角的是定義了一個函數指針,用typedef不太明顯,用using很清晰,另外還可以用於類中成員,右下角所示。左下角是Alias Template的例子,我們日常使用的string實則是basic_string
using
給出了using的使用場景。
noexcept
noexcept是放在函數右括號后,宣稱這個函數不會拋出異常(這里還給出異常的回傳機制,調用foo如果拋出異常,foo會接着往上層拋出異常,如果最上層沒有處理,則會調用terminate函數,terminate函數內部調用abort,使程序退出),noexcept可以接受條件,如上所示,沒有加條件,默認是不會拋出異常,swap函數不發生異常的條件是noexcept(x.swap(y))不會發生異常。
在使用vector和deque的移動構造和移動賦值的時候,如果移動構造和移動賦值沒有加上noexcept,則容器增長的時候不會調用move constructor,效率就會偏低(逐一拷貝),所以后面需要加上noexcept,安心使用。
override
override用於虛函數,上面的virtual void vfunc(int)實際上不是重寫父類的虛函數,而是定義一個新的虛函數,我們的本意是重寫虛函數,但這樣寫編譯器不會報錯,確實沒問題,那如果像下面加上override的話,則會報錯,因為已經告訴了編譯器,我確實要重寫,但寫錯了,沒有重寫,於是就報錯了。
final
final關鍵字用於兩個地方,第一個用在類,用於說明該類是繼承體系下最后的一個類,不要其他類繼承我,當繼承時就會報錯。第二個用在虛函數,表示這個虛函數不能再被override了,再override就會報錯。
decltype
使用decltype關鍵字,可以讓編譯器找到一個表達式它的類型,這個很像typeof的功能,然而已存在的typeof的實現並無完整和一致,所以C++11介紹了一個新的關鍵字,上面給出了一個事例(coll可能離elem很遠)。
decltype的應用有三部分,用作返回值的類型,元編程以及lambda函數的類型
上圖就是用作返回值的類型,圖中第一個代碼塊編譯無法通過,因為return表達式所用的對象沒有在定義域內。C++11則允許另外一種寫法,第二個代碼塊,返回類型用auto暫定,但在后面寫出,用-> decltype(x+y)
, 這里要說明,模板是一種半成品,類型沒有定義,decltype(x+y)
也能是正確的也可能是錯誤的,取決於調用者本身的使用。-> decltype(x+y)
與lambda的返回類似。
上圖是decltype在標准庫中的使用,到處可見。
用於元編程推導實參的類型,由於加了::iterator
, 傳入的實參必須是容器,傳入復數會報錯,這就是模板的半成品特性。
對於lambda函數,很少有人能夠寫出它的類型,而有時就需要知道它的類型,如上定義所示,這時候就可以使用decltype來自動推導lambda函數的類型。
lambdas
C++11介紹了lambdas(可以說是匿名函數或仿函數),允許定義在聲明和表達式中,作為一種內聯函數。如上所示,最簡單的lambda通過一個[]{statements}表示,可以直接加()運行,或者使用auto l = []{statements},l則代表lambda函數,可以在后面進行調用。
上面是lambdas函數的結構類型,中括號[]內部是可以抓取外面的非靜態對象進行函數內部的使用,有以值[=]進行抓取和以引用[&]進行抓取,如果只抓取部分對象,可以進行指定,如上面的x,y,x就是按值進行抓取,y就是按引用進行抓取。小括號()里面則是可以接函數參數,跟普通函數一樣。mutable
可選,指的是以值進行抓取的對象是否可變,可變就需要加上,否則會報錯。throwSepc
是指這個函數可以不可以拋出異常。->retType
指的是lambda函數的返回類型。大括號內部則是函數的主體。
上面是一個事例,這頁幻燈片說的是lambda函數映射類似一個仿函數和mutable的作用,之所以說類似,這是因為如果lambda以值傳遞,則要修改值對象,需要加上mutable,否則會報錯,而仿函數沒有限制。
這頁幻燈片是上頁的比較,要修改以值傳遞的對象,需要加mutable,如果是按引用傳遞的對象,則可以不加,如果修改以值傳遞的對象而不加mutable,則會報錯read-only. 此外,以引用傳遞的對象,不僅會受lambda函數內部的影響,還會受到外部的影響。另外,在lambda函數中,可以申明變量和返回數值。
上圖是編譯器給lambda函數生成得代碼,可以看到就是一個仿函數(重載了小括號操作符)的類,用lambda形式寫非常簡潔,並且要高效一些(inline)。
這張圖的最上面是說每一個lambda函數都是獨特的,要申明lambda對象的類型,可以使用template或者auto進行自動推導。如果需要知道類型,可以使用decltype,比如,讓lambda函數作為關聯容器或者無序容器的排序函數或者哈希函數。上面代碼給出了事例(decltype的第三種用法中的事例),定義了一個lambda函數用cmp表示,用來比較Person對象的大小,傳入到Set容器中去,但根據右邊的set容器的定義,我們傳入的不僅是cmp(構造函數),還要傳入模板的cmp類型(Set內部需要聲明cmp類型),所以必須使用decltype來推導出類型。(如果沒有向構造函數傳入cmp,調用的是默認的構造函數,也就是set() : t(Compare())
, 這里會報錯, 因為Compare()指的是調用默認的lambda構造函數,而lambda函數沒有默認構造函數和賦值函數)
函數對象是很強大的,封裝代碼和數據來自定義標准庫的行為,但需要寫出函數對象需要寫出整個class,這是不方便的,而且是非本地的,用起來也麻煩,需要去看怎樣使用,另外編譯出錯的信息也不友好,而且它們不是inline的,效率會低一些(算法效率還是最重要的)。而lambda函數的提出解決了這個問題,簡短有效清晰,上面的事例很好的說明了這個問題,用lambda要簡短許多,功能一樣,很直觀。
Variadic Template (重磅原子彈)
print函數的例子
Variadic Template是指數量不定,類型不定的模板,這是C++11原子彈級別的炸彈,如上所示的print函數,可以看到接受了不同類型的參數,調用的函數就是擁有Variadic Template的函數,print(7.5, "hello", bitset<16>(377), 42)
運行的時候,首先會7.5作為firstArg,剩余部分就是一包,然后在函數內部,繼續遞歸調用print函數,然后把"hello"作為firstArg, 其余的作為一包,一直遞歸直到一包中沒有數據,調用邊界條件的print(空函數)結束。
函數的...
表示一個包,可以看到,用在三個地方,
-
第一個地方是模板參數
typename...
,這代表模板參數包。 -
第二個就是函數參數類型包(
Type&...
), 指代函數參數類型包。 -
第三個就是函數參數包
args...
,指的是函數參數包。另外,還可以使用
sizeof...(args)
得到包的長度。右邊的是另外一種類型的print,可以和左邊的print共同存在,我測試了一下:
#include <iostream>
#include <bitset>
using namespace std;
void print() {};
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
{
cout << firstArg << endl;
print(args...);
}
template <typename... Types>
void print(const Types&... args)
{
cout << "common print" << endl;
}
int main() {
print(7.5, "hello", bitset<16>(377), 42);
return 0;
}
輸出的結果如下:
7.5
hello
0000000101111001
42
可以看到調用的還是左邊的print,至於為什么,后面再說!
哈希表的例子
上面這個是用variadic template實現哈希表的過程,CustomerHash重載了小括號操作符,內部調用了hash_val,有三個參數,調用的是前面有圓圈1的hash_val,因為其他的hash_val第一參數不符合,然后這個hash_val函數里面設定種子(seed),調用帶有圓圈2的hash_val函數,取出第一個值,調用hash_combine重新設定seed,然后再遞歸調用圓圈2的hash_val, 再重新得到新種子,直到arg...
只有一個參數的時候, 調用圓圈3的hash_val函數,hash_val函數調用hash_combine函數,得到最后的seed,即為哈希值。
tuple
C++11還引入了一種新的容器,名為tuple,可以容納不同類型的數據,左邊是它的簡單實現,關注繼承那三行代碼,可以看到tuple的模板參數是一個Head
和一個包...Tail
,繼承的卻是private tuple<...Tail>
,而tuple<...Tail>
還是tuple,所以又會拆分成tuple<Head, ...Tail>
,不斷遞歸,形成一種遞歸繼承,終止條件就是空的tuple類,在左上角定義的,如果定義tuple<int, float, string>
,它的具體形式如右上角所示,是不斷繼承的結構,這就是能容納不同類型的原因,中上角也是類似的抽象關系。tuple初始化先初始化Head,然后初始化繼承的inherited,繼承的inherited也會類似初始化,直到到達空的tuple,還給出tuple的兩個函數head()和tail(),head()直接返回的是當前類本身的數據(不是從父類繼承過來的),而調用tail()返回this指針(指向當前的那一塊內存),經過向上轉型得到inherited的地址(指向當前繼承的那一塊)。
以上是開頭講的variadic template,現在進入正式講解variadic template的環節。
先回顧了template,一般的模板有函數模板,類模板以及成員模板,強大的還是可變化的模板參數,變化表現在參數個數也表現在參數類型,利用參數個數逐一遞減的特性,實現函數的遞歸調用,同時個數上的遞減也會導致參數類型也逐一遞減,從而實現遞歸繼承(tuple)以及遞歸復合。最下面的是函數使用variadic template一種常見的寫法。
這頁幻燈片前面已經講述了,不過這里給出了之前幻燈片中的一個疑問,print(7.5, "hello", bitset<16>(377), 42)
為什么調用左邊的函數,而不是右邊的,這是因為模板有特化的概念,相對於圓圈3實現的printX(泛化),圓圈1實現的printX更加特化,所以會調用左邊的函數。
printf例子
上面這是使用variadic template實現C語言的printf,很簡潔的寫法。前面的"%d %s %p %f\n"
是第一個參數s,后面的參數構造與print類似,一次取一個對象,參數s用以printf里面的循環條件,當*s
非空時,
-
如果
*s
等於'%'
且下一個字符不等於%
,則打印取出的對象,同時遞歸調用printf函數,要對字符指針進行自加移位。 -
如果上述條件不成立,則打印
*s++
最后的終止條件是args...
為空,打印完了,調用邊界條件的printf,對剩余的*s
進行打印,還要進行%
判斷,因為已經打印完了,還有符合條件的%
,則需要拋出異常。
給定一包數據,找出它們的最大值,也可以通過variadic template實現,不過當數據的類型都相同的時候,無需動用大殺器,使用initializer_list足矣。上面是max使用initializer_list的實現,由於使用initializer_list,所以需要講數據用大括號包起來,編譯器會自動生成initializer_list,然后調用max_element函數得到最大值的地址,然后加*
得到最大值,而max_element是一個模板函數,調用的是__max_element
函數,__max_element
函數內部使用了__iter_less_iter
類得到一個比大小的臨時對象,然后使用臨時對象重載操作符的方法對每一個元素進行比較,最后返回最大值的地址。
上面是variadic template實現的方法, 采用的是遞歸策略,很好懂的。還可以進行改進,講上圖中的int換成模板參數T的話,那么maximum方法就可以接受所有的類型的參數,混合在一起比較(比如double和int混合)。
tuple輸出操作符
tuple重載的輸出流操作符,也使用variadic template,運行右邊的那行代碼,將得到下面黑色的輸出,make_tuple函數是根據參數(可以任意個,內部估計也是使用了variadic template),初始化得到一個tuple,可以看到輸出流操作符得第二個參數就是可變模板參數的tuple,內部調用PRINT_TUPLE類中的靜態print函數,PRINT_TUPLE有三個模板參數,第一個當前索引IDX,第二個是tuple內含有MAX個對象,第三個就是模板參數包。通過get<IDX>(t)
可以得到tuple的第IDX元素,然后進行輸出,依次遞歸調用print函數,如果IDX是最后一個元素了滿足IDX+1==MAX
, 輸出""
,然后調用終止的PRINT_TUPLE::print
函數(空的)完成打印。
tuple補充
上圖之前講過了,這里有一句話很有意思,遞歸調用處理的是參數,使用function template,遞歸繼承處理的是類型,使用的是class template。
不過上述的代碼編譯時不通過的,因為HEAD::type
這個原因(比如int::type是沒有的)。
然后修改成這樣,使用decltype進行類型推導,得到返回類型。不過需要把數據移到上面取,太離譜了。
最終發現直接返回Head就可以了,侯捷老師考慮太復雜了哈哈哈。
之前的tuple是通過遞歸繼承來實現的,上圖展示了如何通過遞歸復合來實現tuple,原理與之前的類似,數據多了Composited類型的m_tail, 依次不斷遞歸,直到最后復合到空的tuple。
variadic template到此結束,真的很強大!
標准庫層面
右值引用
右值引用是為了解決不必要的拷貝以及使能完美轉發而引入的新的引用類型。當右邊的賦值類型是一個右值,左邊的對象可以從右邊的對象中偷取資源而不是重新分配拷貝,這個偷取的過程叫做移動語義。上述給出了事例,a+b和臨時對象就是右值,右值只能出現在右邊,左邊則可以都出現,這里的complex類和string類是由C++作者寫的,引入了不同的修改和賦值,沒有遵守右值的定義,所以它們的事例沒有報錯。方便記憶,可以這里理解右值和左值,可以取地址,有名字的是左值,而不能取地址,沒有名字的是右值。還有一種解釋,右值由將亡值和純右值組成,將亡值如a+b賦給a后就死掉,臨時對象也是一樣,純右值指的是2,'a',true等等。
右值出現,對其進行資源的搬移是合理的,所以引出了兩點,第一點是要有語法告訴編譯器這是右值,第二點是被調用段需要寫出一個專門處理右值的搬移賦值(move assignment)函數。
上述的是測試程序,在vector尾端插入Mystring的臨時對象,調用的vector需要實現帶有右值插入的版本,也就是箭頭指向的版本——insert(..., &&x),insert函數則會調用MyString的拷貝構造函數,為了不進行拷貝,也需要寫出一個右值引用類型的拷貝構造。noexcept是為了讓編譯器知道構造和析構不會拋出異常,當vector增長的時候,move構造函數才會調用起來。上圖中還顯示了關於copy和move的區別,可以看到copy中的數據是有兩份的,其中一份是拷貝過來的,而move操作的數據是只有一份的,原來可能指向臨時對象,現在指向搬移后的對象,原來的對象會設置為空指針。上圖中有一個std:move函數很有幫助,它會將左值對象轉成右值對象,代碼中可以經常用到。
上面的GCC2.9和GCC4.9版的insert函數,可以看到GCC4.9版引入move aware的insert函數。
除了拷貝構造以外,還有拷貝賦值也需要寫一個move版本的。
perfect forwarding
在看perfect forwarding之前,先看看unperfect forwarding,關注一下forward(2)的調用,2是純右值,調用的是forward(int&& i)函數,但在forward(int&& i)函數里面使用i,i就會變為左值,這是我們不想看到的,左值意味着可能會有不必要的拷貝,所以有perfect forwarding.
perfect forwarding可以允許你寫一個函數模板,有任意個參數,透明地轉發給另一個函數,其中參數的本質(可修改性,const,左值,右值)都會在轉發過程中保留下來,使用的是std::forward模板函數。
可以看到forward內部使用了static_cast對傳入的對象進行轉型。move函數也一樣。
move aware class
上面兩頁是帶有move aware的Mystring實現,灰色部分就是move版本的拷貝構造與拷貝賦值。可以看到直接就是淺拷貝,對指針和長度直接賦值,然后將原來對象的內部指針設置為空指針,內部長度為0。而不帶有move的拷貝構造和拷貝賦值都調用了_init_data函數,內部調用的是memcpy函數進行拷貝。還有一點需要注意,由於有了move版本,析構函數需要進行少量修改,當指針為空時,不進行delete操作,此時沒有指向對象了。
move aware測試
與之前的幻燈片相比,多了一個NoMove的參數,這是為了比較copy和move的性能差異。
可以看到在vector容器中,使用copy和move版本的insert函數,差異很大,copy操作花費了更多的時間。而對於直接std::move和傳統的拷貝構造,更是差異巨大。這里雖說只有三百萬個元素,但由於vector有動態增長,所以構造函數調用次數會多於三百萬次。
而對於其他容器而言,構造函數階段差別不大,但move版本還是快一些,當然,std::move與傳統的拷貝賦值還是差異巨大,畢竟一個是拷貝所有的值,一個只是拷貝指針。
vector的拷貝構造與移動構造
可以看到拷貝構造實際上是先分配要拷貝的對象的長度的內存,然后調用copy ctors一一復制.(注意看參數的箭頭)
而移動構造調用的是_M_swap_data函數,內部是指針的交換,c2現在成了c,c沒有意義了,不能再使用。
array
TR1版本的array,內部是一個數組,封裝了一些接口,可以適配算法庫。
GCC4.9版本的array,接口一樣,用到了面向對象的東西,代碼更復雜。
Unordered容器
無序容器內部是通過哈希表實現的,哈希表的結構如上所示,key實際上是一個指針vector,vector的長度叫做buckets(分箱), 每個vector中的指針指向一個鏈表,不同環境的哈希表實現方式不一樣,有的是雙向鏈表,有的是單項鏈表。哈希表是利用鍵值進行取值,比如鍵值為6,則訪問vector第7個指針所指向的鏈表,如果鏈表有多個元素,則按序查找。哈希表都有一個特性,當元素的個數大於buckets時,需要rehashing,將hash表的buckets進行增大,一般是兩倍大左右的質數,然后重新分配。
C++11引入了4種無序容器,分別是unordered_set, unordered_multiset, unordered_map以及unordered_multimap。具體使用將在體系結構那一個大課上寫。
哈希函數
上面沒說如何根據數據得到鍵值,這就要用到哈希函數了,以上GCC4.9版本計算每一個類型的哈希值事例,其中hash
GCC2.9要清楚許多,代碼將每一個類型進行了特化,形成對應了哈希函數,上面的都是整型的。
上面的是關於字符數組的哈希函數,再GCC2.9版本中,沒有提供string的哈希函數,只有字符數組的。
上面的是字符數組的使用事例,結合之前提到的哈希表的結構,可以很清楚的知道具體流程。
GCC2.9許多類型沒有支持,GCC4.9則基本都支持了,思想還是對每種類型進行特化,上面的就是GCC4.9版本的哈希函數,后面幾頁都是接着這一頁的。類__hash_base定義了兩種類型,一種是返回結果(哈希函數的返回結果都是size_t), 另 一種是參數類型,用以給哈希函數進行繼承。上圖左邊給出了指針的特化版本,實現是通過reinterpret_cast對指針進行了轉型,這種轉型的運行期定義的,C++還有另外三種轉型——static_cast, dynamic_cast以及const_cast,一般轉型使用的是static_cast, dynamic_cast用於繼承轉型,const_cast用於去除對象的只讀性,更多細節請查看《more effective C++》的條目二。
對於整型,使用了宏定義簡化相同代碼,因為整型都一樣,直接返回即可。
對於上面的浮點數以及字符串來說,調用的是Hash_impl類的hash函數,該函數調用的是_Hash_bytes函數,然后_Hash_bytes函數只有聲明,沒有定義,侯捷老師認為是該函數是二進制碼函數(編譯好的二進制),所以無法在源代碼中找到。
再探萬用的hash函數
對於數據而言,最小組成單元無非都是整形,浮點型以及字符串,所以對於任意對象,是可以進行hash的,這一節就是要設計萬用的hash函數。
對於某一個類的哈希函數,可以有以上三種寫法,第一種是自定義類,該類是重載()的仿函數,第二種是寫成正兒八經的函數,不過在定義容器時,寫的類型較復雜。第三種使用namespace,將類包在std中,相當於特化此類,這樣定義的時候無需寫哈希函數類型。
為了支持不同個數的變量,使用了之前章節的variadic template,可以傳入任意個數的參數的hash_val函數,具體流程已在variadic template一節講述,可以往回看。
上頁幻燈片中計算seed表達式中的0x9e3779b9是一個特殊的值,黃金比例,為了讓哈希函數生成的鍵足夠亂而引入的。
tuple實例
這里主要示范了一下tuple的一些用法,不細說,看代碼就能看懂,對於代碼中為什么內存大小不是28的問題,侯捷老師不知道,我也沒有查到,在window10-64位電腦的mingw64測試,結果是56,兩倍,在vc2017中測試是64。不同編譯器有區別,具體原因不太清楚。
右下角的是元編程的范例程序,通常編程是操作變量,元編程是操作類型。
舊版tuple
上面兩張幻燈片是最早tuple的實現方式,由於沒有variadic template,所以boost實現tuple使用多個類,最多可以容納15個參數,最早的思想是來自與modern C++ design那本書,作者使用宏定義構建了類似variadic template的方式。
好了,關於侯捷老師的C++11/14課程的筆記到這里就結束了,后面會補充一些C++11其他的內容,這些內容是侯捷老師沒有提到的。