Effective Modern C++ 腦圖
阿新 • • 發佈:2020-11-23
Item01理解模板型別推導
在模板型別推導過程中,具有引用型別的實參會被當成非引用型別處理,換言之,其引用性會被忽略
對萬能引用形參進行推導時,左值實參會進行特殊處理
對按值傳遞的形參進行推導時,若實參型別中帶有const或volatile飾詞,則它們還是會被當做不帶const或volatile飾詞的型別來處理
在模板型別推導過程中,陣列或函式型別的實參會退化成對應的指標,除非它們被用來初始化引用
Item02理解Auto型別推導
auto型別推導通常和模板型別推導相同,但是auto型別推導假定花括號初始化代表std::initializer_list而模板型別推導不這樣做
在C++14中auto允許出現在函式返回值或者lambda函式形參中,但是它的工作機制是模板型別推導那一套方案
Item03理解decltype
decltype總是不加修改的產生變數或者表示式的型別
對於T型別的左值表示式,decltype總是產出T的引用即T&
C++14支援decltype(auto) ,就像auto一樣,推匯出型別,但是它使用自己的獨特規則進行推導
Item04掌握檢視型別推導結果的方法
型別推斷可以從IDE看出,從編譯器報錯看出,從一些庫的使用看出
這些工具可能既不準確也無幫助,所以理解C++型別推導規則才是最重要的
Item05優先選用auto,而非顯式宣告型別宣告
auto變數必須初始化,通常它可以避免一些移植性和效率性的問題,也使得重構更方便,還能讓你少打幾個字
正如Item2和6討論的,auto型別的變數可能會踩到一些陷阱
Item06當auto推導的型別不符合要求時,使用帶顯式型別的初始化物習慣用法
不可見的代理類可能會使auto從表示式中推匯出“錯誤的”型別
顯式型別初始器慣用法強制auto推匯出你想要的結果
Item07在建立物件時注意區分()和{}
在建構函式過載決議中,括號初始化盡最大可能與std::initializer_list引數匹配,即便其他建構函式看起來是更好的選擇
對於數值型別的std::vector來說使用花括號初始化和小括號初始化會造成巨大的不同
在模板類選擇使用小括號初始化或使用花括號初始化建立物件是一個挑戰
Item08優先選用nullptr,而非0或null
優先考慮nullptr而非0和NULL
避免過載指標和整型
Item09優先選用別名宣告,而非typedef
typedef不支援模板化,但是別名宣告支援。
別名模板避免了使用"::type"字尾,而且在模板中使用typedef還需要在前面加上typename
C++14提供了C++11所有型別轉換的別名宣告版本
Item10優先選用限定作用域的列舉型別,而非不限定作用域的列舉型別
C++98的列舉即非限域列舉
限域列舉的列舉名僅在enum內可見。要轉換為其它型別只能使用cast
非限域/限域列舉都支援基礎型別說明語法,限域列舉基礎型別預設是int,非限域列舉沒有預設基礎型別
限域列舉總是可以前置宣告,非限域列舉僅當指定它們的基礎型別時才能前置
Item11優先選用刪除函式,而非private未定義函式
比起宣告函式為private但不定義,使用delete函式更好
任何函式都能delete
,包括非成員函式和模板例項
Item12為意在改寫的函式新增override宣告
為過載函式加上override
成員函式限定讓我們可以區別對待左值物件和右值物件(即*this)
Item13優先選用const_iterator,而非iterator
優先考慮const_iterator而非iterator
在最大程度通用的程式碼中,優先考慮非成員函式版本的begin,end,rbegin等,而非同名成員函式
Item14只要函式不會發射異常,就為其加上noexcept宣告
noexcept是函式介面的一部分,這意味著呼叫者會依賴它、
noexcept函式較之於非noexcept函式更容易優化
noexcept對於移動語義,swap,記憶體釋放函式和解構函式非常有用
大多數函式是異常中立的(譯註:可能拋也可能不拋異常)而不是noexcept
Item15只要有可能使用constexpr,就使用它
constexpr物件是cosnt,它的值在編譯期可知
當傳遞編譯期可知的值時,cosntexpr函式可以產出編譯期可知的結果
Item16保證const成員函式的執行緒安全性
確保const成員函式執行緒安全,除非你確定它們永遠不會在臨界區(concurrent context)中使用
std::atomic可能比互斥鎖提供更好的效能,但是它只適合操作單個變數或記憶體位置
Item17理解特種成員函式的生成機制
特殊成員函式是編譯器可能自動生成的函式:預設構造,析構,拷貝操作,移動操作
移動操作僅當類沒有顯式宣告移動操作,拷貝操作,析構時才自動生成
拷貝構造僅當類沒有顯式宣告拷貝構造時才自動生成,並且如果使用者聲明瞭移動操作,拷貝構造就是delete。拷貝賦值運算子僅當類沒有顯式宣告拷貝賦值運算子時才自動生成,並且如果使用者聲明瞭移動操作,拷貝賦值運算子就是delete。當用戶聲明瞭解構函式,拷貝操作不再自動生成
Item18使用std::unique_ptr管理具備專屬所有權的資源
std::unique_ptr是小巧,高速的,具備只移型別的智慧指標,對託管資源實施專屬所有權語義
預設地,資源析構採用delete運算子來實現,但可以指定自定義刪除器,有狀態的刪除器和採用函式指標實現的刪除器會增加std::unique_ptr型別的物件尺寸
將std::unique_ptr轉換成std::unique_ptr是容易實現的
Item19使用std::unique_ptr管理具備共享所有權的資源
std::shared_ptr為任意共享所有權的資源一種自動垃圾回收的便捷方式
較之於std::unique_ptr,std::shared_ptr物件通常大兩倍,控制塊會產生開銷,需要原子引用計數修改操作
預設資源銷燬是通過delete,但是也支援自定義銷燬器。銷燬器的型別是什麼對於std::shared_ptr的型別沒有影響
避免從原始指標變數上建立std::shared_ptr
Item20對於類似std::shared_ptr但有可能空懸的指標使用std::weak_ptr
使用std::weak_ptr來代替可能空懸的std::shared_ptr
Std::shared_ptr可能的用武之地包括快取,觀察者列表,以及避免std::shared_ptr指標環路
Item21優先選用std::make_unique和std::make_shared_ptr,而非直接使用new
相比於直接使用new表示式,make系列函式消除了重複程式碼,改進了異常安全性,並且對於std::make_shared和std::allocated_shared而言,生成的目的碼尺寸會更小,速度更快
不適於使用make系列函式的場景包括需要定製刪除器,以及期望直接傳遞大括號初始化物
對於std::shared_ptr,不建議使用make系列函式的額外場景包括
-
自定義記憶體管理的類
-
記憶體緊張的系統,非常大的物件,以及存在比指涉到相同物件的std::shared_ptr生存期更久的std::weak_ptr
Item22使用Pimpl習慣用法時,將特殊成員函式的定義放到現實檔案中
pImpl慣用法通過減少在類實現和類使用者之間的編譯依賴來減少編譯時間。
對於std::unique_ptr型別的pImpl指標,需要在標頭檔案的類裡宣告特殊的成員函式,但是在實現檔案裡面來實現他們。即使是編譯器自動生成的程式碼可以工作,也要這麼做
以上的建議只適用於std::unique_ptr,不適用於std::shared_ptr
Item23理解std::move和std::forward
std::move執行到右值的無條件的轉換,但就自身而言,它不移動任何東西
std::forward只有當它的引數被繫結到一個右值時,才將引數轉換為右值
std::move和std::forward在執行期什麼也不做
Item24區分萬能引用和右值引用
如果一個函式模板引數的型別為T&&,並且T需要被推導得知,或者如果一個物件被宣告為auto&&,這個引數或者物件就是一個通用引用
如果型別宣告的形式不是標準的type&&,或者如果型別推導沒有發生,那麼type&&代表一個右值引用
通用引用,如果它被右值初始化,就會對應地成為右值引用;如果它被左值初始化,就會成為左值引用
Item25針對右值引用實施std::move,針對萬能引用實施std::forward
針對右值引用的最後一次使用實施std::move,針對萬能引用的最後一次使用std::forward
作為按值返回的函式的右值引用和萬能引用,依上一條所述採取相同行為
Item26避免依萬能引用型別進行過載
把萬能引用作為過載候選型別,幾乎總會讓該過載版本在始料未及的情況下被呼叫到
完美轉發建構函式的問題尤為嚴重,因為對於非常量的左值型別而言,它們一般都會形成相對於賦值建構函式的更加匹配,並且它們還會劫持派生類中對基類的賦值和移動建構函式的呼叫
Item27熟悉萬能引用型別進行過載的替代方案
如果不使用萬能引用和過載的組合,則替代方案包括使用彼此不同的函式名字,傳遞const T&型別的形參,傳值和標籤分配
經由std::enable_if對模板施加限制,就可以將萬能引用和過載一起使用,不過這種技術控制了編譯器可以呼叫到接受萬能引用的過載版本的條件
萬能引用形參通常在效能方面具備優勢,但在易用性方面一般會有劣勢
Item28理解引用摺疊
引用摺疊會在四種語境中發生:模板例項化,auto型別生成,建立和運用typedef和別名宣告,以及decltype
當編譯器在引用摺疊的語境下生成引用的引用時,結果會變成單個引用。如果原始的引用中有任一引用為左值引用,則結果為左值引用。否則,結果為右值引用
萬能引用就是在型別推導的過程會區別左值和右值,以及會發生引用摺疊的語境中的右值引用
Item29假定移動操作不存在,成本高,未使用
Assume that move operations are not present, not cheap, and not used.
完全瞭解的程式碼可以忽略本Item
Item30熟悉完美轉發的失敗情形
完美轉發的失敗情形,是源於模板型別推導失敗,或推導結果是錯誤的型別
會導致完美轉發失敗的實參種類有大括號初始化物,以值0或null表達的空指標,僅有宣告的整形static const成員變數,模板或過載的函式名字,以及位域
Item31避免預設捕獲方式
預設的按引用捕獲可能會導致懸空引用
預設的按值引用對於懸空指標很敏感(尤其是this指標),並且它會誤導人產生lambda是獨立的想法
Item32使用初始化捕獲將物件移入閉包
使用C ++14的初始化捕獲將物件移動到閉包中。
在C ++11中,通過手寫類或std::bind的方式來模擬初始化捕獲
Item33對auto&&型別的形參使用decltype,以std::forward之
Item34優先選用lambda表示式式,而非std::bind
Lambda式比起使用std::bind而言,可讀性更好,表達力更強,可能效率也更高
僅在C++11中,std::bind在實現移動捕獲,或是繫結到具備模板化的函式呼叫運算子的物件的場合中,可能尚有餘熱可以發揮
Item35優先選用基於任務非基於執行緒的程式設計
std::threadAPI不能直接訪問非同步執行的結果,如果執行函式有異常丟擲,程式碼會終止執行
基於執行緒的程式設計方式關於解決資源超限,負載均衡的方案移植性不佳
基於任務的程式設計方式std::async會預設解決上面兩條問題
Item36如果非同步是必要的,則指定std::launch::async
Std::async的預設氣動策略既允許以非同步方式執行,也允許任務以同步方式執行
如此的彈性會導致使用thread_local便量時的不確定性,隱含著任務可能永遠不會執行,還會影響運用了基於超時的wait呼叫的程式邏輯
如果非同步是必要的,則指定std::launch::async
Item37使std::thread型別物件在所有路徑皆不可聯結
使std::thread型別物件在所有路徑皆不可聯結
在析構時呼叫join可能導致難以除錯的效能異常
在析構時呼叫detach可能是導致難以除錯的未定義行為
在成員列表的最後宣告std::thread型別物件
Item38對變化多端的執行緒控制代碼解構函式行為保持關注
期值的解構函式在常規情況下,僅會析構期值的成員變數
指涉到經由std::async啟動的未推遲任務的共享狀態的最後一個期值會保持阻塞,直至該任務結束
Item39考慮針對一次性事件通訊使用以void為模板型別實參的期值
如果僅為了實現平凡事件通訊,基於條件變數的設計會要求多餘的互斥量,這會給相互關聯的檢測和反應任務帶來的約束,並要求反應任務校驗事件確已發生
使用標誌位的設計可以避免上述問題,但這一設計基於輪詢而非阻塞
條件變數和標誌位可以一起使用,但是這樣的通訊機制設計結果不甚自然
使用std::promise型別物件和期值就可以迴避這些問題,但是一來這個途徑為了共享狀態需要堆記憶體,而僅限於一次性通訊
Item40對併發使用std::atomic,對特種記憶體使用volatile
Std::atomic用於多執行緒訪問的資料,且不用互斥量,它是撰寫併發軟體的工具
Volatile用於讀寫操作不可以被優化掉的記憶體,它是在面對特種記憶體時使用的工具
Item41針對可複製的形參,在移動成本低並且一定會被複制到前提下,考慮將其按值傳遞
對於可複製,移動開銷低,而且無條件複製的引數,按值傳遞效率基本與按引用傳遞效率一致,而且易於實現,生成更少的目的碼
通過建構函式拷貝引數可能比通過賦值拷貝開銷大的多
按值傳遞會引起切片問題,所說不適合基類型別的引數
Item42考慮置入而非插入
從原理上說,置入函式應該有時比對應的插入函式高效,而且不應該有更低效的效能
從實踐上說,置入函式在以下幾個前提成立時,極有可能會執行更快:
-
待新增的值是構造而非賦值的方式加入容器
-
傳遞的實參型別與容器持有之物型別不同
-
容器不會由於存在重複值而拒絕新增的值
置入函式可能會執行在插入函式中被拒絕的型別轉換