在之前介紹 C++20 模組的時候,有提到如果想要在模組內部拆分程式碼的時候,可以使用「module partition」的形式來進行;但是由於 module partition 只是模組內部實作階段的功能,所以其實外部是看不到這些區塊分割、也沒辦法只匯入一部分的。
那如果希望可以把一個大型模組拆分成好幾個小區塊、允許使用者各自匯入該怎麼做呢?
在 C++20 的規範內其實沒有這項功能,不過其實還是可以用「子模組」(submodule)這種不在官方規範的概念來達成這樣的需求。
基本上的概念,就是:
自己定義看起來有結構化名稱的模組、然後自己建立階層關係。
而一般來說,這邊會建議用「.
」來做名字的區隔,例如「MyModule.SubModuleA
」這樣的形式。
像是 C++23 的標準函式庫的模組化就是提供 std
和 std.compat
這兩個模組(參考),這邊也是以「.
」來做名字的區隔。
不過要注意的,是「.
」在這邊其實就是單純的一般字元、對編譯器來說並沒有任何實際的意義,所以這樣的「子模組」其實和一般的模組沒有任何的差別;這點和「:
」是代表模組區塊、在編譯氣來看有實際意義是不一樣的。
這邊繼續沿用之前在講模組分區的例子,這邊把 MyMath
這個模組分成「Types
」和「Compute
」兩個子模組。
這邊把「Types
」這個模組的檔案取名為「MyMath.Types.ixx
」,然後內容是:
export module MyMath.Types; export struct CVec2 { float x; float y; };
然後「Compute
」的模組則可以命名為「MyMath.Compute.ixx
」,內容是:
export module MyMath.Compute; export import MyMath.Types; export CVec2 operator+(const CVec2& a, const CVec2& b) { return { a.x + b.x, a.y + b.y }; }
可以看到,這兩個模組都是以「MyMath
」開頭,雖然在程式裡面沒有意義,但是使用者可以明確地感覺得出來這幾個模組是相關的。
這邊都完成後,可以再用一個大的模組「MyMath
」把這兩個模組包起來、方便使用。這個檔案命名為「MyMath.ixx
」,內容如下:
export module MyMath; export import MyMath.Types; export import MyMath.Compute;
可以看到,MyMath
的內容只有把前面定義的兩個子模組匯入再匯出而已。
而和使用模組分區的時候相比,在這個簡單的例子下,主要就是把「:
」換成「.
」,然後再匯入的時候會需要給完整的模組名稱了。
這樣定義好這三個模組後,之後再使用的時候就可以很方便了~
一般使用的時候只需要匯入整個 MyMath
、就可以使用所有的內容。
import MyMath; int main() { CVec2 v1{ 2, 2 }, v2{ 3,3 }; auto v3 = v1 + v2; }
而和模組分區在外部使用時看不到分區、只能全部匯入不同,使用子模組的概念來實作模組的話,就可以匯入局部模組、而不用全部匯入。
像是如果沒有要進行計算的時候,就可以只匯入 MyMath.Types
:
import MyMath.Types; int main() { CVec2 v1{ 2, 2 }, v2{ 3,3 }; }
某種意義上,使用的彈性比模組分區來的更大了?
而且,和模組分區必須要把所有分區匯入再匯出不同,子模組的設計也不一定要全部包進主要模組內。
比如果這邊又加入了輸入輸出的部分,檔案名稱是「MyMath.IO.ixx
」,內容是:
module; #include <iostream> export module MyMath.IO; export import MyMath.Types; export std::ostream& operator<<(std::ostream& os, const CVec2& v) { return os << '(' << v.x << ", " << v.y << ')'; }
這邊也不一定要回去修改 MyMath.ixx
,就可以直接拿來用了;當需要輸出的時候,只要額外匯入 MyMath.IO
就可以了:
#include <iostream> import MyMath; import MyMath.IO; int main() { CVec2 v1{ 2, 2 }, v2{ 3,3 }; std::cout << (v1 + v2) << "\n";; }
某種程度上,也算是可以很簡單地進行非侵入式的擴充了。
這邊完整的程式可以參考 GitHub 上的檔案(連結)。
基本上,這種「子模組」的設計概念只是一種用來方便開發者拆分大型模組、讓使用者可以局部使用的方法。
而透過這樣的拆分,技術上也有可能可以減少程式、模組的相依性,讓修改模組時需要重新編譯的狀況減少。
整體來說,個人總覺得使用這種子模組的概念來寫似乎比本來的模組分區來的更方便、更有彈性?不過相對地,他就沒辦法做出內部分區這種東西了。
不過如同前面說過的,這邊的「.
」其實對編譯器來說並沒有任何實際的意義,只是單純為了讓人好識別而已。所以上面四個模組雖然看起來像是一個模組和三個子模組,但是其實在系統面會被視為四個一般的模組。
所以如果不想用「.
」的話,也可以自己換成其他合法的子元(不過大部分的特殊字元不能用就是了)、或者都甚至不要也可以。
另外,這邊也會建議把 .ixx
的檔案名稱命名成模組的名稱,這樣編譯器會比較容易找到要匯入的 BMI 檔案;如果兩者不一致的話,有的編譯器可能就會需要另外指定模組對應的 BMI 檔了。