寫在前面
借著公司內和其他小組的一個分享,把自己幾年來C++開發的一點思考總結一下。全篇沒有高屋建瓴的觀點,基本都是些細節方面的注意事項。希望能給大家提供一點幫助。
構建工具
C/C++世界里有不少的構建工具:make、autotools、scons、CMake、Bazel。但近幾年比較流行的,也就是CMake和Bazel。所以這一部分,也就大概對比下這兩個工具吧。
究竟該選擇哪個工具,我覺得可以從如下幾個方面來對比一下:
1. 上手難度
因為Bazel采用了類似Python的語法,所以其學習曲線相比CMake要平緩一些。但當我們考慮上手難度時,除了學習曲線之外,還要考慮文檔的完備性、該工具的通用性等各個角度。當綜合考慮時,我覺得CMake是一個盡管保守但仍舊不錯的選擇。主要原因就在于,CMake幾乎已經成為現在C++的事實標準。使用CMake,就意味著:
- 你可以把你熟悉CMake的技能用在折騰別的C++項目上。而這點之所以重要,是因為你在利用某個第三方庫的時候,往往需要大概研究下它的編譯過程。
- CMake的官方文檔和stackoverflow上的問答也比較完善。一旦遇到一個問題,往往通過搜索引擎能快速的得到答案。
另外,從設計理念上來看,CMake提供的解決方案是改革式的:它并沒有提供一個全新的解決方案,而是和Make、Visual Studio或者其他現有的構建工具來結合使用的。而這就使得你無需丟棄在其他工具上所積累起來的開發經驗——例如你熟悉make工具,哪怕是一個CMake維護的項目,你也可以毫不費力就知道如何來查看編譯參數,以及控制編譯并發度等等。
而對于Bazel則不是如此。Bazel完全以革命者的姿態完整提供了一整套解決方案,所有的使用細節你都要從頭開始。加上文檔的匱乏,這就使得你也得花上一段時間,才能熟悉Bazel。
2. thirdparty的管理
Bazel內置了對thirdparty源碼級別依賴的支持:
- thirdparty可以是用Bazel構建的,也可以不是。對于非Bazel項目,你需要額外為其添加一個Bazel的描述文件。
- thirdparty可以是一個本地項目,也可以是一個git倉庫或者http鏈接
所以總的來看,Bazel對thirdparty支持還是非常友好的。
就這點對比來看,CMake其實做的是不太好的。CMake盡管也有ExternalProject的feature,但根據實際經驗來看,使用和維護都比較的復雜。所以我還是更傾向于寫幾個腳本來下載和編譯這些thirdparty依賴。
這里可以拿我參與維護的Pegasus項目為例。在該項目中,我們依賴了幾個不同類型的項目:
- 從構建工具上來看,這些依賴有使用CMake的,有使用make的,有使用autotools的
- 從來源上來看,有的依賴來自git倉庫,有的來自http鏈接,有的則是從一個大的項目里面挑選了一個更小的模塊使用
- 從代碼的使用方式上來看,有的是直接拿來用,有的還需要稍微修改下源代碼。
而通過shell腳本,這些各種各樣的場景我們都能非常方便、直接、易維護的得以支持。
3. 其它
Bazel和CMake當然還有些其它方面值得對比,但并非一些通用的點,這里就簡單列舉下,不再詳細展開了:
- IDE集成
- 緩存編譯結果,從而加速編譯過程
- 多語言混合變成的支持
- 分布式編譯
- 跨平臺的支持
再補充一個別人的討論鏈接, 有需求的可以參考一下。
編程規范
強烈推薦Google C++ Style。盡管它禁止了很多C++ feature而被很多人黑的很慘,但從工程的角度而言,它的確提供了非常多極其中肯的建議。說到底,編程規范的存在,主要就是可以讓水平參差不齊的工程師們,可以在一起協作出風格較為一致的項目來。
也存在一些工具可以對google規范進行檢查:
因為google的規范文檔對C++ feature的取舍原因講的非常好,這里就不再贅述了。唯一想補充的是異常:
- C++在語法層面對異常支持不太友好:你無法通過函數簽名來得知一個函數到底會拋出哪些異常。例如:
如果這個接口沒有良好文檔或注釋,并且也沒有代碼可翻時,你在調用這個接口時很有可能會漏掉一些錯誤情況——因為它可能拋出異常。更要命的是,一個疏于捕獲的異常一旦觸發,線上的程序就會crash。void GetSomeResource(const char* resource_name);
其實解釋這么多,大家只要和Java中的異常機制對比一下,就高下立判了。對于這個話題,王垠的這篇博客值得一看的。 - 在運維Pegasus項目時,遇到過一個老版本glibc的bug:如果多個線程同時拋出異常,程序會陷入死循環。這個bug的發現也是個有趣的過程,后面我專門寫篇文章展開吧。
- 在禁用異常后,程序就只能用錯誤碼來進行錯誤處理。對于很多項目,大家都采用一套類似的范式,可以參考tensorflow的做法
C++的新特性
如果能使用C++的新特性,當然是盡量使用的好。我自己在開發中,覺得非常方便必須使用的新特性有:
- 智能指針
- 右值,以及C++14中右值得capture
- lambda, bind
- initialize list
想補充說一下的是auto,我自己不是特別喜歡這個feature,也非常贊同google規范中的對auto的限制:僅當可以提高代碼可讀性時,使用auto
這里不由得就想扯起java 10中的var。雖然能方便開發,但覺得更多的是會被濫用。而一個可能被濫用的feature,還不如沒有的好。
第三方utility
在做項目開發的時候,一般會有很多瑣碎的需求,從而也需要很多utility工具包。這里把我遇到的一些需求整理一下:
- 算法和數據結構:stl, boost
- 錯誤碼管理:參見tensorflow
- C語言的字符串封裝:string_view
- 字符串的各種操作、轉換、打印:可以多翻翻abseil, 以及folly,另外也推薦fmtlib
- 線程安全的、無鎖的數據結構、線程池: folly
- google全家桶:gtest,gflags, glog, protobuf, grpc
最后,也推薦下kudu這個項目,里面有自己實現的一些工具包,以及對google開源項目中utility的整理。
單元測試
每個程序員都討厭寫測試。就我自己而言,我覺的單元測試的目的有以下幾個:
- 確保功能的實現和預期一致
- 防止程序在重構的時候出問題
- 給模塊的使用者,提供使用示例
值得一提的是,對于C++項目,除了功能性測試之外,你最好還能讓你的單元測試通過一些自動化工具的檢測,如:
- valgrind:檢查內存泄露,以及非法訪存
- Address Sanitizer:檢測非法訪存
- Thread Sanitizer:檢測線程競爭
寫在最后
自己的整理這些內容時,腦子里反復縈繞的一個問題是:我們在開發一個項目時,所要遵守的各種流程和規范到底是不是真的有必要的?說的更直白一點就是,“代碼潔癖”這東西到底有沒有意義?
我的看法是:代碼潔癖不是一個原則,而是在投入和產出上的一種權衡。如果僅僅快速試錯,那么就不需要維持代碼潔癖,因為你完全不知道你今天寫的代碼究竟能存活多久。而如果是一個馬拉松式的項目,代碼潔癖就值得維持,因為它對于項目的維護的確很有意義。
最后,貼一個自己比較喜歡的C++博客。