為什麼同為系統級編程語言,Rust 能擁有現代構建/包管理工具,C++ 卻不能?
這方面C++欠缺的就是一個模塊系統。
舉例說吧。
假如要做一個C++包管理器,怎麼管理不同庫之間的依賴呢?
容易想到的一個方案就是每個庫都提供一個入口頭文件,編譯一個項目時由包管理器自己生成一個文件,把編譯的項目的這個入口頭文件和所依賴的每個庫的文件都在生成的這個文件裡面#include一次,然後讓編譯器直接去編譯這個生成的文件就好了。現在你有兩個問題。首先是名字空間衝突的問題。這個好說,只要包管理器統一管理名字,讓一個庫只能獨自佔用一個頂級的名字空間就好。然後,假使你的庫是A,依賴B和C兩個庫,B和C又同時依賴一個庫D。那麼這樣的菱形依賴怎麼解決呢?也好辦,讓包管理器在生成編譯文件的時候考慮到順序,使得D的入口文件的包含總是發生在B和C的前面,並且使每個庫只在其中出現一次,否則包管理器報錯。
到目前為止事情還好辦。
還是用上面的例子,你的庫是A,依賴B和C兩個庫,B和C又同時依賴一個庫D。
現在,D有一個類型X,庫B和C都對類型X做了全局重載,比如說重載了X+X,這個時候重複了。這時候包管理器當然要報錯。問題是B和C都是獨立開發的,這個錯要讓誰去修復?難道是你,A的作者?阻礙上面的程序完成構建的規則在C++中稱為ODR,One Definition Rule。
在支持全局重載的其他編程語言中,包括Rust,對這種窘況有一個規則叫做coherence,有時候叫做orphan rule。在上面的例子中,如果換了Rust的情況,類型X歸屬於crate(指一個編譯單元你可以理解為模塊)D,+運算符也有歸屬,是std標準庫。這個時候B和C都不允許在X上做X+【隨便某個類型】的重載。如果B的作者需要做類似的事,需要自己包裝類型X產生一個新的類型Y,然後在Y上做+運算符的全局重載。C++就算要去掉全局重載也是很尷尬的,比方說iostream的靈活性就依賴於全局重載。
如果想像Rust(以及其他ML語言)那樣,可以在C++加個預處理指令放在編譯單元的入口文件的開頭,標明對這個編譯單元所有的全局重載要遵循orphan rule,同時引入外部模塊的概念,這樣類型才能所屬於某個模塊。這樣能夠在編譯的時候報錯,而不是像ODR那樣在鏈接的時候報錯那就晚了。代價就是iostream肯定沒法正常用了。
如果採用像C#那種做法,在需要模塊化的代碼中去掉全局重載的使用,把運算符重載放在靜態成員函數上實現,這樣在模板特化方面沒法做,還是要引入模塊系統,但是不失為另一種做法。其實你想多了,你以為Rust有「現代構建/包管理工具」,然而它並沒有。
C++為什麼沒有好用的包管理器?因為ABI沒統一。Rust統一了嗎?至少在Windows下沒有,再加上動態庫和靜態庫的分歧,再加上Rust和C庫的互操作性,你覺得現在這個cargo真的能把這些問題都搞定?
面對現實吧,cdecl和stdcall就足以把任何這方面的企圖都乾死了。有 yum、apt-get、pacman 等等啊。還有 autotools、pkg-config、cmake 來解決依賴問題,怎麼就沒有了?
並非不能,而是不願,因為綜合下來得不償失。
作為一門30多歲的語言,c++有太多的歷史包袱,更有源碼級C compatible的設計原則,這導致引入module機制會導致用戶代碼大動,而c++世界裡規模過萬行的庫太多太多了。
某些領域裡其實有嘗試,比如visual c++里是支持import導入com組件庫的。
至於構建工具,歷史習慣占很大成分,況且cmake也挺現代的。
今天的rust過了30年也會有各種不合理之處,因地制宜就好。原理不一樣,C++其實有很完善的二進位靜態和動態鏈接機制,所以直接發布二進位的庫反而更容易,由於帶有native代碼,這些庫是不可以跨操作系統、跨架構使用的,因此一般跟應用程序一樣使用操作系統發行版的軟體包管理工具,比如yum、apt-get來發布。其實你從Linux上面獲取一個開源的C++庫是非常簡單的,不管是運行時還是開發環境一般都可以用過yum install一句搞定。C++的編譯比較慢,而且如果使用源代碼會失去動態鏈接的功能,所以一般不直接使用源代碼來分享庫。其他一些語言比如Go設計上就是純源代碼鏈接的,它們不支持通過二進位庫文件進行靜態或動態鏈接,而且它們使用了很多新技術,編譯的速度很快。這樣解決了一些問題比如動態鏈接經常會出現的版本衝突,但其實也喪失了動態鏈接的許多好處。由於直接使用源代碼(或者某種可以跨平台的二進位格式),一般使用專用的包管理工具。
C/C++ Open Source Package Manager
Conan 至少主頁上宣傳的features還都挺有用的聽過一種說法,最可怕的是足夠好。C++現有的東西無論好壞,差不多夠用了。這時要推新東西很難。
好像有一些包管理工具,不過還沒有普及。比如biicode。
推薦閱讀:
※為什麼C++調用空指針對象的成員函數可以運行通過?
※在函數內new一個對象,如果作為引用返回,是不是就可以不用delete了?
※為什麼一個空的class的大小是1個位元組?
※C++非同步回調如何更優雅?
※[C++] 能否設計一個一般的計時函數?