吳詠煒:現(xiàn)代C++大局觀
本文摘錄自Boolan首席咨詢師、著名C++專家吳詠煒老師主講分享的《現(xiàn)代C++大局觀》直播講座。

C++的遠(yuǎn)景
成功語(yǔ)言的煩惱
C++是一門很成功的語(yǔ)言但也有很多問題。我們會(huì)說(shuō)C++太復(fù)雜了,要簡(jiǎn)化;但同時(shí)需要新功能,希望C++能夠演進(jìn);還有不管怎么變,不能把語(yǔ)言搞砸了。
這三點(diǎn)實(shí)際上是不可兼得。
Bjarne洋蔥原則可以部分解決這個(gè)問題即簡(jiǎn)單的事情簡(jiǎn)單做。抽象層次是分層的,如果并不需要最極致的性能,可以采用比較簡(jiǎn)單的做法。但是如果你是個(gè)專家,可以層層深入,但是越往里難度越大,切的越深,哭的越多。
現(xiàn)代C++的功能和慣用法
01?RAII——自動(dòng)、安全的資源管理
C++最重要的特性是 } 。原因在于到了 } 的時(shí)候所有本地變量都會(huì)被析構(gòu)。析構(gòu)函數(shù)和RAII是C++最基本慣用法。

假想一下string的實(shí)現(xiàn)。在默認(rèn)構(gòu)造時(shí)可能會(huì)對(duì)于指針、長(zhǎng)度、容量清零;然后傳進(jìn)去一個(gè)string,我們就要把這個(gè)string復(fù)制一份,把長(zhǎng)度、容量存起來(lái)。重要的是在析構(gòu)的時(shí)候用free把內(nèi)存空間釋放掉。
用RAII慣用法可以管理的不僅僅是內(nèi)存,也可以用來(lái)管理“鎖”。

對(duì)于這樣一個(gè)鎖,用lock_guard鎖住之后,可以做很多同步的工作。然后到了 } 直接停止,自動(dòng)把鎖釋放掉。而不使用RAII的情況下,需要手工lock、unlock,中間有異?;蛘咛崆胺祷鼐蜁?huì)出問題。
RAII是個(gè)遞歸的操作。如果定義了一個(gè)數(shù)的節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)上有string,有string的value,有string的name,還有子節(jié)點(diǎn)的智能指針。節(jié)點(diǎn)銷毀的時(shí)候,會(huì)釋放所有子節(jié)點(diǎn)的引用;如果是唯一的對(duì)子節(jié)點(diǎn)的引用或者是最后的對(duì)子節(jié)點(diǎn)的引用,所有的子節(jié)點(diǎn)也都會(huì)被釋放。所以從管理的角度來(lái)講非常非常的方便。
RAII帶給我們的就是自動(dòng)、安全的資源管理。
02?移動(dòng)語(yǔ)義——高效的大對(duì)象傳遞
引用語(yǔ)義和值語(yǔ)義
首先講一下值語(yǔ)義的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
行為簡(jiǎn)單,符合直接
不容易發(fā)生競(jìng)爭(zhēng)
數(shù)據(jù)嵌套時(shí)內(nèi)存相鄰,性能高
對(duì)象本身可以分配在棧上(棧上內(nèi)存分分配釋放開銷極低)
缺點(diǎn):
容易不小心發(fā)生內(nèi)存復(fù)制
臨時(shí)對(duì)象的情況下,函數(shù)如果返回一個(gè)對(duì)象,它是臨時(shí)對(duì)象,在執(zhí)行完當(dāng)前語(yǔ)句就會(huì)被銷毀。如果要獲得這個(gè)臨時(shí)對(duì)象的資源,可以通過移動(dòng)的方式。我們需要一個(gè)能夠區(qū)別臨時(shí)對(duì)象和其他類型對(duì)象的機(jī)制。這就是C++里右值引用和移動(dòng)語(yǔ)義引用的原因了。
右值引用的基本規(guī)則

我們產(chǎn)生了右值引用本身時(shí)沒有發(fā)生移動(dòng),實(shí)際移動(dòng)操作需要有一個(gè)匹配的函數(shù)來(lái)完成,比如說(shuō)下面這個(gè)string對(duì)象。

核心要點(diǎn)在于有一個(gè)匹配右值引用的移動(dòng)構(gòu)造。在這個(gè)移動(dòng)構(gòu)造里面,會(huì)把右邊右值里面的數(shù)據(jù)全部拿過來(lái),然后把原始數(shù)據(jù)清空,確保析構(gòu)的時(shí)候不會(huì)把已經(jīng)移動(dòng)的內(nèi)存釋放掉。
標(biāo)準(zhǔn)庫(kù)已經(jīng)全面支持了移動(dòng)語(yǔ)義,帶來(lái)一個(gè)小小的副作用就是像這樣簡(jiǎn)單的臨時(shí)字符串拼接操作。

如果從C++98的年代來(lái)看,老程序員會(huì)說(shuō)這樣是錯(cuò)誤的,低效的,不可以接受的。但是到C++11這個(gè)代碼就是正常的,沒有額外開銷的。
移動(dòng)語(yǔ)義帶給我們的是高效的大對(duì)象傳遞。
03?對(duì)象返回和異?!?jiǎn)潔可讀的代碼和完備的錯(cuò)誤處理
這樣的一個(gè)矩陣乘法:R=A x B+C,然后把這個(gè)結(jié)果賦給一個(gè)變量。
傳統(tǒng)C風(fēng)格的代碼會(huì)用錯(cuò)誤碼,永遠(yuǎn)會(huì)返回一個(gè)錯(cuò)誤,代碼非常的啰嗦,有很多檢查,不檢查或者漏檢查就會(huì)出問題。
Armadillo庫(kù)是現(xiàn)代C++的風(fēng)格,有很多現(xiàn)代C++的技巧,也可以把乘法矩陣實(shí)現(xiàn)。

返回對(duì)象的優(yōu)勢(shì)是非常明顯的。
?· 代碼直觀,容易理解
?· 乘法和加法可以組合在一行里寫出來(lái),無(wú)需中間變量
?· 性能高,沒有不需要的復(fù)制發(fā)生(依賴移動(dòng)語(yǔ)義和返回值優(yōu)化)
當(dāng)然這里面也有很多的細(xì)節(jié)需要注意。
· 內(nèi)存分配失敗將導(dǎo)致matrix構(gòu)造失敗
· 矩陣大小不匹配將導(dǎo)致矩陣運(yùn)算失敗
· 任何本地matrix變量分配的內(nèi)存,將在變量離開作用域時(shí)被釋放
· 異常安全可以不需要任何顯式的try...catch
異常當(dāng)然也是有一些問題的。
· 只要啟用異常,代碼就會(huì)有膨脹(約5-15%)
· 異常路徑上的性能損失較大,比錯(cuò)誤碼大很多
· 異??赡軐?dǎo)致錯(cuò)誤發(fā)生點(diǎn)和錯(cuò)誤處理變得不明顯(泛型的要求,模板代碼通常無(wú)法預(yù)知什么情況下會(huì)有異常)
很多地方是不使用異常的,有很多原因有可能是歷史的原因,有可能是對(duì)未知的恐懼等等,如果你所在的項(xiàng)目不使用異常的情況下,你需要問一下到底是為什么。
不使用異常有哪些的后果

對(duì)象返回和異常帶給我們的就是簡(jiǎn)潔可讀的代碼和完備的錯(cuò)誤處理。
04?C++的易用性改進(jìn)——更簡(jiǎn)單的代碼,即使語(yǔ)言在變復(fù)雜
統(tǒng)一初始化
在C++98和C僅用 { } 來(lái)初始化數(shù)組和結(jié)構(gòu)體。到了C++11開始就可以統(tǒng)一使用 { } 來(lái)對(duì)變量進(jìn)行初始化。

用這種語(yǔ)法可以解決一個(gè)問題,叫最令人惱火的語(yǔ)法分析。是說(shuō)一些情況下,寫出的語(yǔ)法有可能讓編譯器認(rèn)為這是一個(gè)函數(shù)聲明,例如下圖:

解決這個(gè)問題可以用 { } 統(tǒng)一初始化規(guī)避:
ifstream ifs{utf8_to_wstring{filename}};
這樣編譯器就會(huì)認(rèn)為這是一個(gè)變量聲明而不是一個(gè)函數(shù)聲明。
總的來(lái)講,我們是推薦使用統(tǒng)一初始化,但如果一個(gè)類使用初始化列表的構(gòu)造函數(shù)時(shí),則只應(yīng)在利用初始化列表構(gòu)造時(shí)使用。
類數(shù)據(jù)成員的默認(rèn)初始化

這里的語(yǔ)法在聲明成員變量的時(shí)候在結(jié)尾加上 { } 初始化代表對(duì)這些變量清零,配對(duì)使用的語(yǔ)法就是一個(gè)默認(rèn)聲明的默認(rèn)構(gòu)造函數(shù),聲明point的時(shí)候默認(rèn)里面清零。
對(duì)于string可以寫成這樣的語(yǔ)法

同時(shí)也可以進(jìn)一步寫成這樣

利用用戶定義字面量可以寫出非常直觀的代碼。

這些都是非常方便使用的,同時(shí)這個(gè)語(yǔ)法我們自己也可以用來(lái)定義字面量,唯一的限制是非標(biāo)準(zhǔn)的用戶定義字面量必須以下劃線打頭,如下圖:

這些易用性改進(jìn)帶給我們更簡(jiǎn)單的代碼,即使語(yǔ)言在變復(fù)雜。
05?模板——安全,高性能的代碼復(fù)用
模板我們討論最常用的示例sort。sort一般來(lái)講最后一個(gè)參數(shù)可以是函數(shù)對(duì)象、可以是lamda表達(dá)式,也可以是一個(gè)函數(shù)指針。不管怎么樣,代表的意思都是怎么進(jìn)行比較。

這兩種寫法是等價(jià)的,都是比較兩個(gè)整數(shù)是否滿足小于關(guān)系。
在C++里面用sort打開O2優(yōu)化情況下,sort比qsort快了好幾倍。

模板有好處也有壞處,好處是零開銷抽象,高性能;類型安全的代碼復(fù)用。但是出錯(cuò)信息不友好,動(dòng)輒可達(dá)數(shù)百行。
怎么對(duì)模板參數(shù)進(jìn)行約束

傳統(tǒng)上我們可以用SFINAE檢測(cè)編譯器成員,C++17里面有些不錯(cuò)的改進(jìn),C++20也進(jìn)一步簡(jiǎn)化。

可以定義一個(gè)概念叫reservable;然后定義兩個(gè)存在,reservable C和typename C,C++的編譯器會(huì)自動(dòng)幫你選擇你該用哪一個(gè)。
模板帶給我們的就是安全,高性能的代碼復(fù)用。
06?編譯期編程——靈活性和新的可能,無(wú)額外運(yùn)行期開銷
接下來(lái)我們來(lái)討論一個(gè)燒腦的話題,編譯期編程。這是用編譯期編程來(lái)寫階乘,產(chǎn)生的匯編代碼是這樣的。


然后我們可以寫出編譯期的條件語(yǔ)句,用一個(gè)布爾值用作模板參數(shù)來(lái)代表?xiàng)l件,然后用類型作為when 和else的分支。可以產(chǎn)生這樣的結(jié)果。

C++從C++11開始就有constexpr,也在不斷演化。目的就是明確標(biāo)明編譯期的常量和編譯期計(jì)算函數(shù)。用傳統(tǒng)的C++式的語(yǔ)法來(lái)寫編譯期計(jì)算代碼,同樣的階乘,可以寫成這樣一個(gè)函數(shù)。

這個(gè)函數(shù)factorial可以執(zhí)行計(jì)算,采用了遞歸式的調(diào)用但是用循環(huán)寫也是完全可以的??梢灾苯勇暶魉愠龅慕Y(jié)果是編譯期的常量,然后就可以輸出和前面一樣的編譯結(jié)果。
然后還有if constexpr可以簡(jiǎn)化前面的寫法,可以把代碼并成一個(gè)分支。如果你的C容器滿足reserve條件的,編譯的時(shí)候就會(huì)產(chǎn)生對(duì)reserve的調(diào)用。
編譯期編程帶給我們的就是靈活性和新的可能,無(wú)額外運(yùn)行期開銷。
展望C++20
C++是一種成熟的古老的語(yǔ)言,這個(gè)語(yǔ)言本身雖然不完美,但是在變得越來(lái)越好。C++會(huì)越來(lái)越靠近完全的類型和資源安全。既能對(duì)新手更友好,還能對(duì)專家保留強(qiáng)大的定制能力,達(dá)到極致的性能。


12月9日晚20:00
《現(xiàn)代C++白皮書》線上發(fā)布會(huì)
等你來(lái)參與
