漫谈 C++ 的各种检查
以下文章來(lái)源于BOTManJL?,作者BOT Man
What you don't use you don't pay for. (zero-overhead principle)?—— Bjarne Stroustrup
背景閱讀
在學(xué)習(xí)了?Chromium/base 庫(kù)(筆記)后,我體會(huì)到了一般人和?優(yōu)秀工程師?的差距 —— 擁有較高的個(gè)人素質(zhì)固然重要,但更重要的是能?降低開(kāi)發(fā)門檻,讓其他人更快的融入團(tuán)隊(duì),一起協(xié)作(尤其像 Chromium?開(kāi)源項(xiàng)目?由社區(qū)維護(hù),開(kāi)發(fā)者水平參差不齊)。
沒(méi)吃過(guò)豬肉,但見(jiàn)過(guò)豬跑。?
項(xiàng)目中,降低開(kāi)發(fā)門檻的方法有很多:除了 制定?代碼規(guī)范、劃分?功能模塊、完善?單元測(cè)試?(unit test)、推行?代碼審查?(code review)、整理?相關(guān)文檔?之外,針對(duì)強(qiáng)類型的編譯語(yǔ)言 C++,Chromium/base 庫(kù)加入了大量的?檢查?(check)。
為什么代碼中需要各種檢查?在 C++ 中調(diào)用一個(gè)函數(shù)、使用一個(gè)類、實(shí)例化一個(gè)模板時(shí),對(duì)傳入的參數(shù)、使用的時(shí)機(jī),往往會(huì)有很多?限制?(constraint/restriction)(例如,數(shù)值參數(shù)不能傳入負(fù)數(shù)、對(duì)象的訪問(wèn)不是線程安全的、函數(shù)調(diào)用不能重入);而處理限制的方法有很多:
口口相傳:在?代碼審查?時(shí),有經(jīng)驗(yàn)的開(kāi)發(fā)者 向 新手開(kāi)發(fā)者 傳授經(jīng)驗(yàn)(很容易失傳)
文檔說(shuō)明:在?相關(guān)文檔?中,提示使用者 功能模塊的各種隱含限制(很容易被忽略)
檢查限制:在合理劃分?功能模塊?的前提下,對(duì)模塊的隱含限制 進(jìn)行檢查,并加入針對(duì)檢查的?單元測(cè)試(最安全的保障,單元測(cè)試即文檔)
本文主要分享 Chromium/base 庫(kù)中使用的一些限制檢查。
漫談 C++ 的各種檢查??1 編譯時(shí)檢查
編譯時(shí)靜態(tài)檢查,主要依靠 C++ 語(yǔ)言提供的?語(yǔ)法支持/靜態(tài)斷言?和?編譯器擴(kuò)展?實(shí)現(xiàn) —— 在檢查失敗的情況下,編譯失敗。
1.1 測(cè)試設(shè)施
如何確保代碼中添加的檢查有效呢?最高效的方法是:為 “檢查” 添加單元測(cè)試。但對(duì)于 編譯時(shí)檢查 遇到了一個(gè)?難點(diǎn)?—— 如果檢查失敗,那么編譯就無(wú)法通過(guò)。
為此,Chromium 支持?編譯失敗測(cè)試?(no-compile test):
單元測(cè)試文件中,每個(gè)用例通過(guò)?#ifdef?切割
每個(gè)用例中,標(biāo)明 編譯失敗后期望的 報(bào)錯(cuò)細(xì)節(jié)
通過(guò)?#define?運(yùn)行各個(gè)用例
在編譯失敗后,檢查 報(bào)錯(cuò)細(xì)節(jié) 是否和預(yù)期一致
對(duì)應(yīng)的單元測(cè)試文件后綴為?*_unittest.nc,通過(guò)?nocompile.gni?加入單元測(cè)試工程。
1.2 可拷貝性檢查
C++ 語(yǔ)言本身有很多編譯時(shí)檢查(例如 類的成員訪問(wèn)控制?(member access control)、const?關(guān)鍵字 在編譯成匯編語(yǔ)言后,不能反編譯還原),但 C++ 對(duì)象默認(rèn)是可拷貝的,從而帶來(lái)了許多問(wèn)題(參考?資源管理小記)。
尤其是?多態(tài)?(polymorphic)?類的默認(rèn)拷貝行為,一般都不符合預(yù)期:
C.67: A polymorphic class should suppress copying
C.130: For making deep copies of polymorphic classes prefer a virtual clone function instead of copy construction/assignment
為此,Chromium 提供了兩個(gè)?常用的宏:
DISALLOW_COPY_AND_ASSIGN?用于禁用類的 拷貝構(gòu)造函數(shù) 和 拷貝賦值函數(shù)
DISALLOW_IMPLICIT_CONSTRUCTORS?用于禁用類的 默認(rèn)構(gòu)造函數(shù) 和 拷貝行為
由于 Chromium 大量使用了 C++ 的多態(tài)特性,這些宏隨處可見(jiàn)。
1.3 參數(shù)類型檢查
Chromium 還基于?現(xiàn)代 C++ 元編程?技術(shù),通過(guò)?static_assert?進(jìn)行靜態(tài)斷言。
在之前寫(xiě)的?深入 C++ 回調(diào)?中分析了:
?Chromium 的base::Callback <>?+?
base::Bind()?回調(diào)機(jī)制,提到了相關(guān)的靜態(tài)斷言檢查。
base::Bind?為了?處理失效的(弱引用)上下文,針對(duì)弱引用指針base::WeakPtr擴(kuò)展了base::IsWeakReceiver檢查,判斷弱引用的上下文是否有效;并通過(guò)靜態(tài)斷言檢查傳入?yún)?shù),強(qiáng)制要求使用者遵循?弱引用檢查的規(guī)范:
base::Bind?不允許直接將?`this` 指針?綁定到 類的成員函數(shù) 上,因?yàn)?this?裸指針可能失效 變成野指針
base::Bind?不允許綁定?lambda 表達(dá)式,因?yàn)?base::Bind?無(wú)法檢查 lambda 表達(dá)式捕獲的 弱引用 的 有效性
base::Bind?只允許將?base::WeakPtr?指針綁定到?沒(méi)有返回值的(返回?void)類的成員函數(shù) 上,因?yàn)?當(dāng)弱引用失效時(shí)不調(diào)用回調(diào),也沒(méi)有返回值
base::Callback區(qū)分回調(diào)只能執(zhí)行一次還是可以多次,通過(guò)引用限定符?(reference qualifier)?&&?/?const &,區(qū)分在對(duì)象處于 非 const 右值 / 其他狀態(tài)時(shí)的?Run?成員函數(shù),只允許一次回調(diào)?base::OnceCallback?在非 const 右值狀態(tài)下調(diào)用?Run?函數(shù),保證嚴(yán)謹(jǐn)?shù)?資源管理語(yǔ)義:
base::OnceClosure?cb;?std::move(cb).Run();????????//?OK base::OnceClosure?cb;?cb.Run();???????????????????//?not?compile const?base::OnceClosure?cb;?cb.Run();?????????????//?not?compile const?base::OnceClosure?cb;?std::move(cb).Run();??//?not?compile另外,靜態(tài)斷言檢查還廣泛應(yīng)用在 Chromium/base 的容器、智能指針?模板的實(shí)現(xiàn)中,用于生成可讀性更好的實(shí)例化錯(cuò)誤信息。
1.4 線程標(biāo)記檢查
最新的 Chromium 使用了 Clang 編譯,通過(guò)擴(kuò)展?線程標(biāo)記?(thread annotation),靜態(tài)分析線程安全問(wèn)題。(參考:Thread Safety Annotations for Clang - DeLesley Hutchins)
Chromium/base 的單元測(cè)試文件 :
thread_annotations_unittest.nc?描述了一些 鎖的錯(cuò)誤使用場(chǎng)景(假設(shè)數(shù)據(jù) data 被鎖 lock 保護(hù),定義標(biāo)記為 Type data GUARDED_BY(lock);):
訪問(wèn) data 之前,忘記獲取 lock
獲取 lock 之后,忘記釋放 lock
這些錯(cuò)誤能在編譯時(shí)被 Clang 檢查到,從而編譯失敗。
2 運(yùn)行時(shí)檢查
運(yùn)行時(shí)動(dòng)態(tài)檢查,主要基于 Chromium/base 庫(kù)提供的?斷言?DCHECK/CHECK?實(shí)現(xiàn) —— 如果斷言失敗,運(yùn)行著的程序會(huì)立即終止。
其中,DCHECK?只對(duì)調(diào)試版?(debug)?有效,而?CHECK?也可用于發(fā)布版?(release)?—— 從而避免在發(fā)布版進(jìn)行無(wú)用的檢查。
2.1 測(cè)試設(shè)施
檢查的方法很直觀 —— 構(gòu)造一個(gè)檢查失敗的場(chǎng)景,期望斷言失敗。
Chromium/base?基礎(chǔ)設(shè)施中的EXPECT_DCHECK_DEATH提供了這個(gè)功能,對(duì)應(yīng)的單元測(cè)試文件后綴為?*_unittest.cc。
2.2 數(shù)值溢出檢查
C++ 的數(shù)值類型,都是固定大小的標(biāo)量類型?—— 如果存儲(chǔ)數(shù)值超出范圍,會(huì)導(dǎo)致溢出?(overflow)。
例如,嘗試通過(guò)?使用無(wú)符號(hào)數(shù) 避免出現(xiàn)負(fù)數(shù),往往是一個(gè)典型的徒勞之舉。(比如?unsigned(0) - unsigned(1) == UINT_MAX,參考?ES.106: Don’t try to avoid negative values by using?unsigned)
為此,Chromium 的?base/numerics?提供了一個(gè) 無(wú)依賴?(dependency-free)、僅頭文件?(header-only)?的模板庫(kù),處理數(shù)值溢出問(wèn)題:
base::StrictNumeric/base::strict_cast<>()?編譯時(shí) 阻止溢出?—— 如果 類型轉(zhuǎn)換 有溢出的可能性,通過(guò)靜態(tài)斷言報(bào)錯(cuò)
base::CheckedNumeric/base::checked_cast<>()?運(yùn)行時(shí) 檢查溢出?—— 如果 數(shù)值運(yùn)算/類型轉(zhuǎn)換 出現(xiàn)溢出,立即終止程序
base::ClampedNumeric/base::saturated_cast<>()?運(yùn)行時(shí) 截?cái)噙\(yùn)算?—— 如果 數(shù)值運(yùn)算/類型轉(zhuǎn)換 出現(xiàn)溢出,對(duì)計(jì)算結(jié)果?截?cái)?/strong>?(non-sticky saturating)?處理
2.3 線程相關(guān)檢查
最新的 Chromium/base 線程模型引入了線程池,并支持了序列?(sequence)?的概念 —— 相對(duì)于線程池中的普通任務(wù)亂序調(diào)度,同一序列的任務(wù) 能保證被順序調(diào)度 —— 因此,推薦使用邏輯序列 而不是物理線程:
同一物理線程 只能同時(shí)運(yùn)行 一個(gè)邏輯序列,使得 序列模型 等效于 單線程模型
同一物理線程 可以用于運(yùn)行 多個(gè)邏輯序列,提高 物理線程 的利用率
線程/序列 相關(guān)的檢查主要依賴于?線程/序列 本地存儲(chǔ):
每個(gè)線程有獨(dú)立的?`base::ThreadLocalStorage`?
線程本地存儲(chǔ)?(thread local storage, TLS)
每個(gè)序列有獨(dú)立的?`base::SequenceLocalStorageSlot`?
序列本地存儲(chǔ)?(sequence local storage, SLS)
當(dāng) 邏輯序列 被放到 物理線程 上執(zhí)行時(shí),把當(dāng)前序列的 SLS 關(guān)聯(lián)到 執(zhí)行線程的 TLS
2.3.1 線程安全檢查
很多時(shí)候,某個(gè)對(duì)象只會(huì)在?同一線程/序列?中?創(chuàng)建/訪問(wèn)/銷毀:
正常情況下,無(wú)競(jìng)爭(zhēng)?(contention-free)?模型沒(méi)必要保證?線程安全?(thread-safety),因?yàn)?線程同步操作/原子操作 會(huì)帶來(lái)?不必要的開(kāi)銷
異常情況下,一旦被 多線程同時(shí)使用,訪問(wèn)沖突導(dǎo)致?數(shù)據(jù)競(jìng)爭(zhēng)?(data race),可能出現(xiàn) 未定義行為
為此,Chromium 借助:
base::ThreadChecker/base::SequenceChecker
檢查對(duì)象是否只在 同一線程/序列 中使用:
[THREAD|SEQUENCE]_CHECKER(checker)?創(chuàng)建并關(guān)聯(lián) 線程/序列?checker
DCHECK_CALLED_ON_VALID_THREAD|SEQUENCE?檢查或關(guān)聯(lián)?checker?和 當(dāng)前執(zhí)行環(huán)境的 線程/序列
DETACH_FROM_THREAD|SEQUENCE?解除?checker?和 線程/序列 的關(guān)聯(lián)
另外,發(fā)布版的檢查實(shí)現(xiàn)為?空對(duì)象,即總能通過(guò)檢查
實(shí)現(xiàn)的?核心思想?非常簡(jiǎn)單:
線程/序列 創(chuàng)建時(shí),通過(guò) TLS/SLS 記錄 當(dāng)前線程/序列的 ID(例如 線程 ID、序列 ID)
checker?構(gòu)造時(shí),記錄 當(dāng)前線程/序列的 ID
checker?檢查時(shí),讀取 當(dāng)前線程/序列的 ID,和?checker?記錄的 ID 比較
checker?析構(gòu)時(shí),先執(zhí)行檢查(可以提前 解除關(guān)聯(lián))
另外,checker?讀寫(xiě) 數(shù)據(jù)成員時(shí),需要進(jìn)行互斥的 線程同步操作(鎖)
在[sec|通知迭代檢查] 提到,base::ObserverList借助?iteration_sequence_checker_?在迭代時(shí)檢查 對(duì)象操作 是否線程/序列安全。
2.3.2 線程限制檢查
程序中常常會(huì)有一些?特殊用途的線程(例如 客戶端 UI 主線程),而這些線程往往有著?特殊的限制(例如,UI 線程要求保持?響應(yīng)性?(responsive),實(shí)時(shí)響應(yīng)用戶輸入)。
為此,Chromium 借助 :
base::ThreadRestrictions?檢查可能涉及線程限制的函數(shù)在當(dāng)前執(zhí)行的線程上是否允許:
阻塞?(blocking)?操作
主要包括文件 I/O 操作(有可能被系統(tǒng)緩存,從而不阻塞)
可能導(dǎo)致線程 交出 CPU 執(zhí)行機(jī)會(huì),進(jìn)入 wait 狀態(tài)
同步原語(yǔ)?(sync primitive)
執(zhí)行 線程同步操作
可能導(dǎo)致程序 死鎖?(deadlock)/卡頓?(jank)
CPU 密集工作?(CPU intensive work)
超過(guò) 100ms CPU 時(shí)間的操作
可能導(dǎo)致程序 卡頓?(jank)
單例?(singleton)?操作
對(duì)于 非泄露型?`base::Singleton`,會(huì)在?`base::AtExitManager`?注冊(cè) “退出時(shí)銷毀單例對(duì)象”
如果主線程先退出,在?base::AtExitManager?中銷毀單例,導(dǎo)致仍在運(yùn)行的 non-joinable 線程再訪問(wèn)單例時(shí),出現(xiàn)野指針崩潰
實(shí)現(xiàn)的?核心思想?也很簡(jiǎn)單:
通過(guò) TLS 記錄 當(dāng)前線程的限制情況(每種限制用一個(gè) TLS?bool?存儲(chǔ))
對(duì)于 可能涉及限制的函數(shù),調(diào)用前先檢查 當(dāng)前線程 是否允許某個(gè)限制
在最新的Chromium/base 中,線程限制檢查被進(jìn)一步封裝為:
base::ScopedBlockingCall,并應(yīng)用于大量文件 I/O 相關(guān)函數(shù)中。
2.3.3 死鎖檢查
Chromium 通過(guò)?base::internal::CheckedLock
檢查 死鎖?(deadlock)。
實(shí)現(xiàn)的?核心思想?非常簡(jiǎn)單 ——?檢查等待鏈?zhǔn)欠癯森h(huán):
維護(hù)一個(gè) 全局的 <從每個(gè) lock 到其 predecessor lock> 映射表(創(chuàng)建時(shí)添加,銷毀時(shí)移除)
維護(hù)一個(gè) 當(dāng)前線程的 <已獲取 lock> 列表(TLS 存儲(chǔ);獲取時(shí)記錄,釋放時(shí)移除)
創(chuàng)建時(shí),斷言 predecessor 已創(chuàng)建(如果 predecessor 不存在,可能順序錯(cuò)誤)
獲取時(shí),斷言 predecessor 是當(dāng)前線程最近獲取的 lock(若不是,可能順序錯(cuò)誤)
2.4 觀察者模式檢查
在之前寫(xiě)的?令人抓狂的觀察者模式?中,介紹了如何通過(guò) :
Chromium/base 提供的base::ObserverList,檢查觀察者模式的一些潛在問(wèn)題。
2.4.1 生命周期檢查
由于觀察者和被觀察者的生命周期往往是解耦的,所以總會(huì)出現(xiàn)一些陰差陽(yáng)錯(cuò)的問(wèn)題:
觀察者先銷毀
問(wèn)題:若?base::ObserverList?通知時(shí)不檢查 觀察者是否有效,可能導(dǎo)致 野指針崩潰
解決:觀察者繼承于?`base::CheckedObserver`
在通知前?base::ObserverList?檢查觀察者弱引用?base::WeakPtr?的有效性
被觀察者先銷毀
問(wèn)題:若?base::ObserverList?銷毀時(shí)不檢查 觀察者列表是否為空,可能導(dǎo)致 被觀察者銷毀后,觀察者不能再移除(野指針崩潰)
解決:模板參數(shù)?check_empty?若為?true,在析構(gòu)時(shí)斷言 “觀察者已被全部移除”
2.4.2 通知迭代檢查
觀察者可能在?base::ObserverList?通知時(shí),再訪問(wèn)同一個(gè)?base::ObserverList?對(duì)象:
添加觀察者
問(wèn)題:是否需要在 本次迭代中,繼續(xù)通知 新加入的觀察者
解決:被觀察者參數(shù)?`base::ObserverListPolicy`?
決定迭代過(guò)程中,是否通知 新加入的觀察者
移除觀察者
問(wèn)題:循環(huán)內(nèi)(間接)刪除節(jié)點(diǎn),導(dǎo)致迭代器失效(崩潰)for(auto it = c.begin(); it != c.end(); ++it) c.erase(it);
解決:觀察者節(jié)點(diǎn)?MarkForRemoval()?標(biāo)記為 “待移除”,然后等迭代結(jié)束后移除
通知迭代重入
問(wèn)題:許多情況下,若不考慮 重入情況,可能會(huì)導(dǎo)致?死循環(huán)問(wèn)題
解決:模板參數(shù)?allow_reentrancy?若為?false,在迭代時(shí)斷言 “正在通知迭代時(shí) 不允許重入”
銷毀被觀察者
問(wèn)題:需要立即停止 迭代過(guò)程,讓所有迭代器 全部失效
解決:通過(guò)特殊的?`base::internal::WeakLinkNode`?+
雙向鏈表?`base::LinkedList`?存儲(chǔ)?base::ObserverList?所有的迭代器;在?base::ObserverList?析構(gòu)時(shí),將迭代器 標(biāo)記為無(wú)效(自動(dòng)停止迭代),并 移除、銷毀
線程安全問(wèn)題
問(wèn)題:由于?base::ObserverList?不是線程安全的,在通知迭代時(shí),需要保證其他操作在 同一線程/序列
解決:被觀察者成員?iteration_sequence_checker_
在迭代開(kāi)始時(shí)關(guān)聯(lián)序列,在結(jié)束時(shí)解除關(guān)聯(lián),在迭代過(guò)程中檢查 移除觀察者/通知重入/銷毀被觀察者 操作是否序列安全(參考 [sec|線程安全檢查])
和?base::Singleton?一樣,Chromium/base 的設(shè)計(jì)模式實(shí)現(xiàn) 堪稱 C++ 里的典范 —— 無(wú)論是功能上,還是性能上,均為 “人無(wú)我有,人有我優(yōu)”。
寫(xiě)在最后??站在巨人的肩膀上。—— 艾薩克·牛頓
Chromium/base 庫(kù)一直在?迭代、優(yōu)化,學(xué)習(xí)、借鑒?許多其他優(yōu)秀的開(kāi)源項(xiàng)目。例如,[sec|線程標(biāo)記檢查] 使用的標(biāo)記就來(lái)源于?abseil。
由于 Chromium/base 改動(dòng)頻繁,本文某些細(xì)節(jié)?可能會(huì)過(guò)期。如果有什么新發(fā)現(xiàn),歡迎補(bǔ)充~ ?
總結(jié)
以上是生活随笔為你收集整理的漫谈 C++ 的各种检查的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 报名|腾讯技术开放日·5G技术专场
- 下一篇: 腾讯游戏自研学术成果:基于图分割的网络表