幾天前,C++20 草案已經獲得了標準委員會的全票通過,C++2a 草案討論的幾個重要內容,比如“概念(concept)”、“范圍庫(Ranges Library)”、“協程(Coroutines)” 和 “模塊(Module)” 都加到 C++20 的標準中了,剩下的就是看編譯器廠商的支持速度了,目前看 CLANG 、GCC 和 Microsoft 是比較積極的三家,語言特性和庫支持的最快的是 GCC,其次是 CLANG 和 Visual C++,估計最快到年底就能看到支持全部 C++ 20 特性的編譯器了,

圖(1)C++ 20 的 Big Four
毫無疑問,在目前主流的編程語言中,C++ 是最難掌握的編程語言,沒有之一,我常常和朋友調侃,C++ 是最適合做高校考試用的語言,因為 C++ 的內容繁雜,知識點多,當然,“坑”也多,非常適合出題考試,從易到難,大學四年考題都不會有重復的,除了對初學者門檻太高,傳統的 C++ 語言特性上支持的也很弱,與其他編程語言相比,做同樣的功能,C++ 往往需要寫更多的代碼,不過從 C++11 開始,一直到 C++17,這種情況開始有了明顯地改善,各種之前被 C++ 社區諷刺為“語法糖”的語言特性逐步被添加到 C++ 語言規范中,比如 lambda 運算式,比如基于范圍的 for 回圈,應該說,這都是被 Python “逼”的,其實不僅僅是 C++ 在學 Python,就連 Java 這個濃眉大眼的家伙,也是學得各種“真香”呢,
這次 C++20 新增的四大語言特性,最讓我碎碎念的就是 Module 了,之前曾經有傳言,說 Module 可能會被推遲到 C++23 發布,不過好在 Module 最后還是“擠”進了C++ 20 ,為什么這么期待 Module,這得從 C++ 的幾個痛點說起,
首先是頭檔案的各種問題,C/C++ 語言使用頭檔案和源檔案分離方式迫使程式員養成介面與實作分離的設計習慣的哲學實驗事實上是失敗了,除了極少的優質專案,大部分的C++專案的源代碼組織并沒有有效地實施這個原則,反而引入了一大堆頭檔案難題,extern關鍵字的“巧用”更是讓這個問題雪上加霜,對初學者來說,找不到頭檔案是最大的“痛”,它就在那兒,為什么編譯器就是“視而不見”?那是因為你的包含路徑設定不正確,什么絕對路徑,什么相對路徑,讓很多還沒入門的初學者過早地體會了“碼”生的艱難,學習 C++,從入門到放棄,也許 include 一個“永遠”找不到的頭檔案就夠了,
對于一個大一點的專案來說,頭檔案之間那種“剪不斷、理還亂”的關系最讓人頭疼不已,頭檔案之間的回圈依賴、互相依賴,你中有我,我中有你,“要用這個介面,只要包含那個叫 A 的頭檔案就行了,easy!”,你的同事們是否都這樣信誓旦旦地跟你說過類似的話?大部分情況下你會發現這個叫 A 的頭檔案并不單純,它可能還有“七大姑八大姨”分別位于不同的代碼目錄中,如果你不設定好頭檔案的搜索路徑,編譯器會告訴你人生有多難,當 Java 的程式員在軟體架構層面上考慮物件之間的依賴關系時,C++ 的程式員還在為了解決編譯問題而試著調整各個頭檔案的包含順序,唉,說多了都是淚,
C++ 的另一個痛點就是編譯速度了,相對于 C++ 的各種“難用”,編譯速度太慢才是 C++ 最大的詬病,本人曾在一個大型 C 語言專案中引入了一個 C++ 模塊,編譯的時候編譯器對 C 的源檔案和 C++ 的源檔案的處理速度明顯不在一個數量級上,GCC 在編譯 C 語言的源檔案的時候,處理速度飛起,輸出資訊刷屏的速度眼花繚亂,但是在處理 C++ 的源檔案的時候,基本上就是以每秒 1 - 2 個這樣的目視可見的速度推進,C++ 因為引入了泛型和模板庫,支持多載,這使得它的預處理需要做很多事情,慢也是必然的,但是對于一個大型專案來說,這種慢會嚴重拖累自動化編譯、測驗和部署的速度,換句話說,就是不“敏捷”,
現在看看 C++20 的 Module 能給我們帶來什么,在 Modernes C++ 網站上,可以窺得一些細節,C++20 的 Module 承諾了五個目標:
-
更快的編譯時間(Faster compile times)
C++ 的編譯器在處理一個源代碼檔案的時候,首先要做的就是用相應的頭檔案的內容替換 #include 預編譯指令,這就存在一個問題,對每一個源代碼檔案編譯器都要重復一遍內容替換,這會占用大量的處理器時間,替換完的源檔案長度也會膨脹數倍,一個源代碼檔案的處理速度不僅要看這個檔案中有多少代碼,還要看它包含了多少頭檔案,使用 Module,只需要 #import 一次(編譯器只需要對匯入的 Module 做一次決議處理),就可以在所有的源代碼檔案中使用,沒有頭檔案的替換動作,也不需要重復決議處理 Module,這對編譯時間將是一個巨大的優化,
-
宏的隔離(Isolation of macros)
只要你使用 C++ 的時間足夠長,你就會碰到這樣的代碼:
#ifdef XXXX
#undef XXXX
#endif
?
#define XXXX ...
初次看到這樣多此一舉的代碼,心里肯定會想這是什么鬼?其實見怪不怪,肯定是某個家伙的也定義了一個同樣名為 XXXX 的宏,只是值不一樣而已,對于同名的宏,如果值也一樣,編譯器一般“睜一只眼閉一只眼”就放過去了,但是如果值不一樣,大多數編譯器的默認選項也只是提示一個編譯告警,如果你碰巧忽略了這個編譯告警,等待你的很可能是痛苦的 Debug 程序,菜鳥們經常抱怨:“我定義的宏的值明明是 5,為什么賦值給變數后變成這個奇怪的值?” 那是因為別人也定義了同名的宏,并且恰巧頭檔案處理順序在你的前面,要么調整頭檔案的包含順序,要么用上面那段代碼,這就是 C++ 程式員的日常作業內容,Module 的使用,可以緩解這種困惑,首先,從語意上講,可以用 module_name.XXXX 來隔離宏,其次,Module 只需要匯入一次,同名宏的先后關系與只與匯入順序一致,不會因為頭檔案的包含順序混亂讓對某個宏的值你摸不著頭腦,
-
表達代碼的邏輯結構(Express the logical structure of the code)
從設計層面上考慮,頭檔案其實包含了過多的實作細節,并不適合向外展示,Module 則很好的規避了這一點,首先,作為一個二進制的包,你不想展示給用戶的內容,都可以隱藏起來,只將必要的部分標記為 export 即可,其次,你可以按照代碼的實作邏輯設計各個 Module,它們可以自由組合形成不同功能的邏輯包提供給客戶,這都比給客戶一堆平面表達的頭檔案,然后讓客戶自己選擇組合使用更能滿足客戶的需求,
-
讓頭檔案成為“過去式”(Make header files superfluous)
一個 C++ 專案只要對同一使用的 Module 管理規范,可徹底解決各種包含路徑問題,當你想使用某個介面的時候,只要 #import A就行了,再也不用關心該死的頭檔案搜索路徑了,誰說這不“香”呢?毫無疑問,有了 Module,再也不會有人走回頭路用頭檔案了,話又說回來,這也讓 C++ 的代碼與 Java 代碼的相似度又增進了一層,搞不好以后要看源代碼檔案的擴展名才能區分,
-
擺脫丑陋的宏環境(Get rid of an ugly macro workarounds)
前面講過,C++ 的編譯器在對 #include 指令包含的頭檔案的處理方式是原地展開替換,假如一個源檔案包含了頭檔案 A 和頭檔案 B,而頭檔案 B 又不巧間接包含了頭檔案 A(這種情況在一個大型的 C++ 專案中幾乎是常態),那么 A 就會在這個源檔案中被展開替換兩次,確實是這樣的,編譯器不僅浪費時間做了展開替換,還會抱怨符號重復定義的錯誤,所以你會看到每個頭檔案幾乎都是這樣用宏環境包裹著自己:
#ifndef THIS_HEAD_FILE_H
#define THIS_HEAD_FILE_H
?
.....
#endif //THIS_HEAD_FILE_H
也許看的時間長了,覺得這樣的代碼很規范,其實這是一種多么無奈的選擇,還是上面那句話:不用頭檔案,就不用再浪費時間給每個頭檔案寫這種宏環境了,
最后,說說C++20 Module 特性讓我失望的地方,那就是沒有統一語言層面上的包管理器,從標準上看,依然是準備留給編譯器廠商或第三方機構做這個事情,這不僅讓我想起來 C++ 到現在都沒有一個語言層面上的 UI 庫,雖然第三方的 UI 庫倒是不少,但是基本上處于“生殖隔離”的狀態,互不通用,編程方法千差萬別,也各有優缺點,常常讓 C++ 程式員無所適從,對于包管理器,微軟推出了 NuGet,但是由于跟自家的 IDE 捆綁太緊密,并沒有得到 C++ 社區的廣泛支持,Python 之所以好用,是因為 Python 語言的締造者和整個 Python 社區共同維護一個統一的包管理器,對各種庫提供統一的包裝介面,使用簡單,學習也簡單,從現在開始到 C++23 發布還有不到三年時間,希望 C++ 標準委員會能領導整個社區弄一個統一的包管理器,對各種庫提供一致的介面封裝,避免出現同一個庫有多個 Module 封裝版本的情況出現,讓 C++ 程式員再次無所適從,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/102841.html
標籤:其他
上一篇:51單片機點亮一個LED
下一篇:OKR筆記3
