模組 (C++20 起)
大多數 C++ 專案使用多個轉譯單元(translation units),因此需要在這些單元間共享宣告與定義。為此,使用標頭檔非常普遍,例如標準程式庫,其宣告可藉由引入對應的標頭檔來提供。
模組是一種用於在轉譯單元間共享宣告與定義的語言特性。它們是標頭檔某些使用場景的替代方案。
模組與命名空間是正交的(互不影響)。
// helloworld.cpp export module helloworld; // module declaration import <iostream>; // import declaration export void hello() // export declaration { std::cout << "Hello world!\n"; }
// main.cpp import helloworld; // import declaration int main() { hello(); }
目錄 |
[編輯] 語法
export(選填) module 模組名稱 模組分割區 (選填) 屬性 (選填) ; |
(1) | ||||||||
export 宣告 |
(2) | ||||||||
export { 宣告序列 (選填) } |
(3) | ||||||||
export(選填) import 模組名稱 屬性 (選填) ; |
(4) | ||||||||
export(選填) import 模組分割區 屬性 (選填) ; |
(5) | ||||||||
export(選填) import 標頭名稱 屬性 (選填) ; |
(6) | ||||||||
module;
|
(7) | ||||||||
module : private;
|
(8) | ||||||||
[編輯] 模組宣告
轉譯單元可以包含模組宣告,這種情況下它被視為模組單元。若提供模組宣告,它必須是轉譯單元的首個宣告(除了稍後會提到的全域模組片段外)。每個模組單元都關聯到一個在模組宣告中指定的模組名稱(以及選擇性的分割區)。
export(選填) module 模組名稱 模組分割區 (選填) 屬性 (選填) ; |
|||||||||
模組名稱由一個或多個以點號分隔的識別字組成(例如:mymodule, mymodule.mysubmodule, mymodule2...)。點號本身沒有內在含義,但通常用於表示階層關係。
若模組名稱或模組分割區中的任何識別字被定義為物件式巨集,則程式格式錯誤。
具名模組(named module)是所有具有相同模組名稱的模組單元的集合。
宣告中包含 export 關鍵字的模組單元稱為模組介面單元;所有其他模組單元稱為模組實作單元。
對於每個具名模組,必須有且僅有一個未指定模組分割區的模組介面單元;此模組單元稱為主模組介面單元。其匯出的內容在匯入對應的具名模組時可用。
// (each line represents a separate translation unit) export module A; // declares the primary module interface unit for named module 'A' module A; // declares a module implementation unit for named module 'A' module A; // declares another module implementation unit for named module 'A' export module A.B; // declares the primary module interface unit for named module 'A.B' module A.B; // declares a module implementation unit for named module 'A.B'
[編輯] 匯出宣告與定義
模組介面單元可以匯出宣告(包括定義),這些宣告可被其他轉譯單元匯入。若要匯出宣告,可在其前加上 export 關鍵字,或將其置於 export 區塊中。
export 宣告 |
|||||||||
export { 宣告序列 (選填) } |
|||||||||
export module A; // declares the primary module interface unit for named module 'A' // hello() will be visible by translations units importing 'A' export char const* hello() { return "hello"; } // world() will NOT be visible. char const* world() { return "world"; } // Both one() and zero() will be visible. export { int one() { return 1; } int zero() { return 0; } } // Exporting namespaces also works: hi::english() and hi::french() will be visible. export namespace hi { char const* english() { return "Hi!"; } char const* french() { return "Salut!"; } }
[編輯] 匯入模組與標頭單元
模組透過匯入宣告進行匯入。
export(選填) import 模組名稱 屬性 (選填) ; |
|||||||||
在給定具名模組的模組介面單元中匯出的所有宣告與定義,將在該使用匯入宣告的轉譯單元中可用。
匯入宣告可在模組介面單元中匯出。亦即,若模組 B 匯出並匯入了 A,則匯入 B 也會讓 A 的所有匯出內容可見。
在模組單元中,所有匯入宣告(包括匯出匯入)必須排列在模組宣告之後,且在所有其他宣告之前。
/////// A.cpp (primary module interface unit of 'A') export module A; export char const* hello() { return "hello"; } /////// B.cpp (primary module interface unit of 'B') export module B; export import A; export char const* world() { return "world"; } /////// main.cpp (not a module unit) #include <iostream> import B; int main() { std::cout << hello() << ' ' << world() << '\n'; }
#include 不應在模組單元中使用(全域模組片段除外),因為所有包含的宣告與定義會被視為模組的一部分。取而代之的是,標頭檔可以作為標頭單元,透過匯入宣告匯入。
export(選填) import 標頭名稱 屬性 (選填) ; |
|||||||||
標頭單元是從標頭檔合成的獨立轉譯單元。匯入標頭單元將使其所有定義與宣告可被存取。預處理器巨集也可存取(因為匯入宣告會被預處理器識別)。
然而,與 #include 不同的是,在匯入宣告處已經定義的預處理巨集不會影響標頭的處理。這在某些情況下可能不便(有些標頭使用預處理巨集作為組態方式),此時就需要使用全域模組片段。
/////// A.cpp (primary module interface unit of 'A') export module A; import <iostream>; export import <string_view>; export void print(std::string_view message) { std::cout << message << std::endl; } /////// main.cpp (not a module unit) import A; int main() { std::string_view message = "Hello, world!"; print(message); }
[編輯] 全域模組片段
模組單元可以在前面加上全域模組片段,這可用於在無法匯入標頭檔的情況下(特別是標頭檔使用預處理巨集作為組態時)包含標頭檔。
module;
預處理指令 (選填) 模組宣告 |
|||||||||
若模組單元具有全域模組片段,則其首個宣告必須是 module;。接著,在全域模組片段中僅能出現預處理指令。隨後,一個標準的模組宣告標誌著全域模組片段的結束與模組內容的開始。
/////// A.cpp (primary module interface unit of 'A') module; // Defining _POSIX_C_SOURCE adds functions to standard headers, // according to the POSIX standard. #define _POSIX_C_SOURCE 200809L #include <stdlib.h> export module A; import <ctime>; // Only for demonstration (bad source of randomness). // Use C++ <random> instead. export double weak_random() { std::timespec ts; std::timespec_get(&ts, TIME_UTC); // from <ctime> // Provided in <stdlib.h> according to the POSIX standard. srand48(ts.tv_nsec); // drand48() returns a random number between 0 and 1. return drand48(); } /////// main.cpp (not a module unit) import <iostream>; import A; int main() { std::cout << "Random value between 0 and 1: " << weak_random() << '\n'; }
[編輯] 私有模組片段
主模組介面單元可以在後面加上私有模組片段,這允許模組被表現為單一轉譯單元,同時不讓模組的所有內容對匯入者可見。
module : private;
宣告序列 (選填) |
|||||||||
私有模組片段終止了模組介面單元中可能影響其他轉譯單元行為的部分。若模組單元包含私有模組片段,它將是該模組中唯一的模組單元。
export module foo; export int f(); module : private; // ends the portion of the module interface unit that // can affect the behavior of other translation units // starts a private module fragment int f() // definition not reachable from importers of foo { return 42; }
[編輯] 模組分割區
模組可以擁有模組分割區單元。它們是模組宣告中包含模組分割區的模組單元,分割區以冒號 : 開頭,並放置在模組名稱之後。
export module A:B; // Declares a module interface unit for module 'A', partition ':B'.
一個模組分割區代表正好一個模組單元(兩個模組單元不能指定同一個模組分割區)。它們僅在具名模組內部可見(具名模組外部的轉譯單元不能直接匯入模組分割區)。
模組分割區可被同一個具名模組的模組單元匯入。
export(選填) import 模組分割區 屬性 (選填) ; |
|||||||||
/////// A-B.cpp export module A:B; ... /////// A-C.cpp module A:C; ... /////// A.cpp export module A; import :C; export import :B; ...
模組分割區中的所有定義與宣告,對匯入的模組單元皆為可見,無論是否匯出。
模組分割區可以是模組介面單元(當其模組宣告具有 export 時)。它們必須由主模組介面單元進行匯出匯入,其匯出的陳述式將在該模組被匯入時可見。
export(選填) import 模組分割區 屬性 (選填) ; |
|||||||||
/////// A.cpp export module A; // primary module interface unit export import :B; // Hello() is visible when importing 'A'. import :C; // WorldImpl() is now visible only for 'A.cpp'. // export import :C; // ERROR: Cannot export a module implementation unit. // World() is visible by any translation unit importing 'A'. export char const* World() { return WorldImpl(); }
/////// A-B.cpp export module A:B; // partition module interface unit // Hello() is visible by any translation unit importing 'A'. export char const* Hello() { return "Hello"; }
/////// A-C.cpp module A:C; // partition module implementation unit // WorldImpl() is visible by any module unit of 'A' importing ':C'. char const* WorldImpl() { return "World"; }
/////// main.cpp import A; import <iostream>; int main() { std::cout << Hello() << ' ' << World() << '\n'; // WorldImpl(); // ERROR: WorldImpl() is not visible. }
[編輯] 模組擁有權
一般來說,若宣告出現在模組單元的模組宣告之後,則它被附著於(attached to)該模組。
若實體的宣告被附著於某個具名模組,則該實體只能在該模組中定義。此類實體的所有宣告都必須附著於同一個模組。
若宣告被附著於具名模組且未被匯出,則宣告的名稱具有模組連結(module linkage)。
export module lib_A; int f() { return 0; } // f has module linkage export int x = f(); // x equals 0
export module lib_B; int f() { return 1; } // OK, f in lib_A and f in lib_B refer to different entities export int y = f(); // y equals 1
若同一實體的兩個宣告被附著於不同的模組,則程式格式錯誤;若兩者均不可互相觸及,則無需診斷。
/////// decls.h int f(); // #1, attached to the global module int g(); // #2, attached to the global module
/////// Module interface of M module; #include "decls.h" export module M; export using ::f; // OK, does not declare an entity, exports #1 int g(); // Error: matches #2, but attached to M export int h(); // #3 export int k(); // #4
/////// Other translation unit import M; static int h(); // Error: matches #3 int k(); // Error: matches #4
以下宣告不附著於任何具名模組(因此宣告的實體可以在模組外定義):
export module lib_A; namespace ns // ns is not attached to lib_A. { export extern "C++" int f(); // f is not attached to lib_A. extern "C++" int g(); // g is not attached to lib_A. export int h(); // h is attached to lib_A. } // ns::h must be defined in lib_A, but ns::f and ns::g can be defined elsewhere (e.g. // in a traditional source file).
[編輯] 附註
| 功能測試巨集 | 數值 | 標準 | 功能 |
|---|---|---|---|
__cpp_modules |
201907L |
(C++20) | 模組 — 核心語言支援 |
__cpp_lib_modules |
202207L |
(C++23) | 標準程式庫模組 std 與 std.compat |
[編輯] 關鍵字
private, module, import, export
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯應用於之前的 C++ 標準。
| DR | 應用於 | 出版時的行為 | 正確的行為 |
|---|---|---|---|
| CWG 2732 | C++20 | 不明確是否可匯入的標頭檔可以 對匯入點的預處理器狀態作出反應 |
無反應 |
| P3034R1 | C++20 | 模組名稱與模組分割區可能 包含被定義為物件式巨集的識別字 |
禁止 |