共用方式為


具名模組教學課程 (C++)

本教學課程是關於建立 C++20 模組。 模組會取代標頭檔。 您將瞭解模組如何改善標頭檔。

在本教學課程中,您將了解如何:

  • 建立和匯入模組
  • 建立主要模組介面單元
  • 建立模組分割區檔案
  • 建立模組單元實作檔案

必要條件

本教學課程需要 Visual Studio 2022 17.1.0 或更新版本。

在本教學課程中處理常式代碼範例時,您可能會收到 IntelliSense 錯誤。 IntelliSense 引擎的工作正在趕上編譯器。 IntelliSense 錯誤可以忽略,而且不會防止程式碼範例建置。 若要追蹤 IntelliSense 工作的進度,請參閱此問題

什麼是 C++ 模組

標頭檔是宣告和定義在 C++ 中的原始程式檔之間共用的方式。 標頭檔很脆弱,難以撰寫。 它們可能會根據您納入的順序,或未定義或未定義的宏,以不同的方式編譯。 它們可能會使編譯時間變慢,因為它們會針對包含它們的每個來源檔案重新處理。

C++20 引進了將 C++ 程式元件化的新式方法: 模組

如同標頭檔,模組可讓您跨原始程式檔共用宣告和定義。 但與標頭檔不同,模組不會外泄巨集定義或私人實作詳細資料。

模組更容易撰寫,因為它們的語意不會因為巨集定義或已匯入的其他專案、匯入的順序等等而變更。 它們也可讓消費者更容易控制可見的內容。

模組提供標頭檔不會的額外安全性保證。 編譯器和連結器會一起運作,以防止可能發生的名稱衝突問題,並提供更強的一個定義規則 ( ODR ) 保證。

強擁有權模型可避免在連結時名稱之間發生衝突,因為連結器會將匯出的名稱附加至匯出它們的模組。 此模型可讓 Microsoft Visual C++ 編譯器防止連結在相同程式中報告類似名稱的不同模組所造成的未定義行為。 如需詳細資訊,請參閱 強式擁有權

模組是由編譯成二進位檔的一或多個原始程式碼檔案所組成。 二進位檔案描述模組中的所有匯出類型、函式和範本。 當來源檔案匯入模組時,編譯器會在包含模組內容的二進位檔中讀取。 讀取二進位檔案比處理標頭檔快得多。 此外,每次匯入模組時,編譯器都會重複使用二進位檔案,節省更多時間。 因為模組建置一次,而不是每次匯入時,建置時間可能會大幅減少,有時會大幅減少。

更重要的是,模組沒有標頭檔所發生的脆弱問題。 匯入模組並不會變更模組的語意,也不會變更任何其他匯入模組的語意。 匯入模組的來源檔案看不到宏、預處理器指示詞和未匯出的名稱。 您可以依任何順序匯入模組,而且不會變更模組的意義。

模組可以與標頭檔並存使用。 如果您要移轉程式碼基底以使用模組,因為您可以分階段執行這項功能,此功能就很方便。

在某些情況下,標頭檔可以匯入為標頭單位,而不是以檔案的形式 #include 匯入。 標頭單位是先行編譯標頭檔 (PCH) 的建議替代方案 。 它們比 共用 PCH 檔案更容易設定和使用,但可提供類似的效能優勢。 如需詳細資訊,請參閱 逐步解說:在 Microsoft Visual C++ 中建置和匯入標頭單位。

您的程式碼可以使用相同專案中的模組,或任何參考的專案,自動使用靜態程式庫專案的專案對專案參考。

建立專案

當我們建置簡單的專案時,我們將探討模組的各個層面。 專案會使用模組來實作 API,而不是標頭檔。

在 Visual Studio 2022 或更新版本中,選擇 [建立新專案 ],然後選擇 [主控台應用程式 ] [C++] 專案類型。 如果無法使用此專案類型,當您安裝 Visual Studio 時,可能尚未選取 [使用 C++ 進行桌面開發] 工作負載。 您可以使用Visual Studio 安裝程式來新增 C++ 工作負載。

為新專案指定名稱 ModulesTutorial 並建立專案。

因為模組是 C++20 功能,請使用 /std:c++20/std:c++latest 編譯器選項。 在方案總管 ,以滑鼠右鍵按一下專案名稱 ModulesTutorial ,然後選擇 [ 屬性 ]。 在 [屬性頁] 對話方塊中,將 [組態] 變更 [所有組態 ] 和 [平臺 ] 變更為 [ 所有平臺 ]。 選取 左側樹狀檢視窗格中的 [組態屬性 > 一般]。 選取 C++ 語言標準 屬性。 使用下拉式清單將屬性值變更為 ISO C++20 Standard (/std:c++20)。 選取 [ 確定 ] 以接受變更。

A screenshot of the ModulesTutorial property page with the left pane open to Configuration Properties > General, and the C++ Language Standard dropdown open with ISO C++20 Standard (/std:c++20) selected

建立主要模組介面單元

模組包含一或多個檔案。 其中一個檔案必須是所謂的 主要模組介面單元 。 它會定義模組匯出的內容;也就是說,模組的匯入工具會看到什麼。 每個模組只能有一個主要模組介面單位。

若要新增主要模組介面單元,請在 方案總管 ,以滑鼠右鍵按一下 [來源檔案 ],然後選取 [ 新增 > 模組 ]。

Add item dialog in solution explorer with Add > Module... highlighted to illustrate where to click to add a module.

在出現的 [ 新增專案 ] 對話方塊中,為新模組指定名稱 BasicPlane.Figures.ixx ,然後選擇 [ 新增 ]。

所建立模組檔案的預設內容有兩行:

export module BasicPlane;

export void MyFunc();

export module第一行中的關鍵字宣告此檔案是模組介面單位。 這裡有一個微妙點:對於每個具名模組,必須只有一個模組介面單元,沒有指定的模組分割。 該模組單元稱為 主要模組介面單元

主要模組介面單元是您宣告來源檔案匯入模組時要公開的函式、類型、範本、其他模組和模組分割區的位置。 模組可以包含多個檔案,但只有主要模組介面檔案可識別要公開的內容。

將檔案的內容 BasicPlane.Figures.ixx 取代為:

export module BasicPlane.Figures; // the export module keywords mark this file as a primary module interface unit

這一行會將此檔案識別為主要模組介面,並提供模組名稱: BasicPlane.Figures 。 模組名稱中的句點對編譯器沒有特殊意義。 期間可用來傳達模組的組織方式。 如果您有多個模組檔案一起運作,您可以使用期間來指出考慮區隔。 在本教學課程中,我們將使用期間來指出 API 的不同功能區域。

此名稱也是「具名模組」中「具名」的來源。 屬於此模組一部分的檔案會使用此名稱將自己識別為具名模組的一部分。 具名模組是具有相同模組名稱的模組單元集合。

我們應該先討論我們將實作的 API,再進一步討論。 這會影響我們下一步做出的選擇。 API 代表不同的圖形。 我們只會在此範例中提供幾個圖形: PointRectanglePoint 是用來做為更複雜的圖形的一部分,例如 Rectangle

為了說明模組的某些功能,我們會將此 API 分解成部分。 其中一個部分是 Point API。 另一個部分將是 Rectangle 。 假設此 API 會成長為更複雜的專案。 分割對於分隔考慮或緩和程式碼維護很有用。

到目前為止,我們已建立將公開此 API 的主要模組介面。 現在讓我們建置 Point API。 我們希望它成為本課程模組的一部分。 基於邏輯組織的原因,以及潛在的建置效率,我們想要讓 API 的這個部分能夠自行輕鬆理解。 若要這樣做,我們將建立 模組分割 區檔案。

模組分割檔案是模組的片段或元件。 其唯一之處是,它可以視為模組的個別部分,但僅限於模組內。 模組分割區無法在模組外部取用。 模組分割區有助於將模組實作分割成可管理的部分。

當您將分割區匯入主要模組時,不論其是否匯出,主要模組都會看到其所有宣告。 分割區可以匯入任何屬於具名模組的資料分割介面、主要模組介面或模組單元。

建立模組分割區檔案

Point 模組分割區

若要建立模組分割區檔案,請在 方案總管 以滑鼠右鍵按一下 [來源檔案 ],然後選取 [ 新增 > 模組 ]。 將檔案 BasicPlane.Figures-Point.ixx 命名為 ,然後選擇 [ 新增 ]。

因為它是模組分割區檔案,因此我們已將連字號和分割區的名稱新增至模組名稱。 這個慣例可協助在命令列案例中編譯器,因為編譯器會根據模組名稱使用名稱查閱規則來尋找資料分割的已 .ifc 編譯檔案。 如此一來,您就不需要提供明確的 /reference 命令列引數,即可尋找屬於模組的資料分割。 它也有助於依名稱組織屬於模組的檔案,因為您可以輕鬆地查看哪些檔案屬於哪個模組。

將 的內容 BasicPlane.Figures-Point.ixx 取代為:

export module BasicPlane.Figures:Point; // defines a module partition, Point, that's part of the module BasicPlane.Figures

export struct Point
{
    int x, y;
};

檔案開頭為 export module 。 這些關鍵字也是主要模組介面的開始方式。 使這個檔案不同的是模組名稱後面冒號 ( : ),後面接著分割區名稱。 此命名慣例會將檔案識別為 模組分割 區。 因為它會定義分割區的模組介面,所以它不會被視為主要模組介面。

名稱 BasicPlane.Figures:Point 會將此分割區識別為模組 BasicPlane.Figures 的一部分。 (請記住,名稱中的句號對編譯器沒有特殊意義)。 冒號表示這個檔案包含名為 的模組分割區,該 Point 分割區屬於 模組 BasicPlane.Figures 。 我們可以將此分割區匯入至屬於此具名模組一部分的其他檔案。

在此檔案中 export ,關鍵字可 struct Point 讓取用者看見。

Rectangle 模組分割區

我們將定義的下一個分割區是 Rectangle 。 使用與先前相同的步驟建立另一個模組檔案:在方案總管中 ,以滑鼠右鍵按一下 [來源檔案 ],然後選取 [新增 > 模組]。 命名檔案 BasicPlane.Figures-Rectangle.ixx,並選取 [新增]

將 的內容 BasicPlane.Figures-Rectangle.ixx 取代為:

export module BasicPlane.Figures:Rectangle; // defines the module partition Rectangle

import :Point;

export struct Rectangle // make this struct visible to importers
{
    Point ul, lr;
};

// These functions are declared, but will
// be defined in a module implementation file
export int area(const Rectangle& r);
export int height(const Rectangle& r);
export int width(const Rectangle& r);

檔案的 export module BasicPlane.Figures:Rectangle; 開頭是 ,它會宣告模組分割區是模組 BasicPlane.Figures 的一部分。 :Rectangle新增至模組名稱的 會將它定義為模組 BasicPlane.Figures 的分割區。 它可以個別匯入到屬於此具名模組一部分的任何模組檔案中。

接下來, import :Point; 示範如何匯入模組分割區。 語句 import 會將模組分割區中的所有匯出類型、函式和範本設為可見。 您不需要指定模組名稱。 編譯器知道此檔案屬於模組, BasicPlane.Figures 因為 export module BasicPlane.Figures:Rectangle; 檔案頂端的 。

接下來,程式碼會匯出某些函式的定義 struct Rectangle 和宣告,這些函式會傳回矩形的各種屬性。 export關鍵字指出是否要讓模組的取用者看見它之前的內容。 它用來在模組外部顯示函 area 式 、 heightwidth

匯入模組單位是否具有 export 關鍵字,都可以看見模組分割區中的所有定義和宣告。 關鍵字 export 會控管當您在主要模組介面中匯出分割區時,定義、宣告或 typedef 是否會顯示在模組外部。

模組的取用者會以數種方式顯示名稱:

  • 將 關鍵字 export 放在您想要匯出的每個類型、函式等前面。
  • 例如,如果您放在 export 命名空間前面, export namespace N { ... } 則會匯出大括弧內定義的所有專案。 但是,如果您在模組中其他地方定義 namespace N { struct S {...};} ,則 struct S 模組的取用者無法使用。 無法使用,因為命名空間宣告不是前面 export ,即使有另一個命名空間具有相同名稱。
  • 如果不應該匯出類型、函式等,請省略 export 關鍵字。 其他屬於模組一部分的檔案,但不會看見模組的匯入工具。
  • 使用 module :private; 來標記私用模組分割區的開頭。 私人模組分割區是模組的一個區段,其中宣告只能看見該檔案。 匯入此模組的檔案或其他屬於此課程模組的檔案都看不到它們。 將其視為檔案靜態本機的區段。 此區段只能在檔案內顯示。
  • 若要讓匯入的模組或模組分割可見,請使用 export import 。 下一節會顯示一個範例。

撰寫模組分割區

既然我們已定義 API 的兩個部分,讓我們將它們結合在一起,讓匯入此模組的檔案可以完整存取它們。

所有模組分割區都必須公開為其所屬模組定義的一部分。 分割區會在主要模組介面中公開。 開啟檔案 BasicPlane.Figures.ixx ,這個檔案會定義主要模組介面。 以下列專案取代其內容:

export module BasicPlane.Figures; // keywords export module marks this as a primary module interface unit

export import :Point; // bring in the Point partition, and export it to consumers of this module
export import :Rectangle; // bring in the Rectangle partition, and export it to consumers of this module

開頭 export import 的兩行在這裡是新的。 如此組合時,這兩個關鍵字會指示編譯器匯入指定的模組,並讓此模組的取用者看到它。 在此情況下,模組名稱中的冒號 ( : ) 表示我們正在匯入模組分割區。

匯入的名稱不包含完整的模組名稱。 例如,分割 :Point 區已宣告為 export module BasicPlane.Figures:Point 。 然而,我們在這裡正在匯入 :Point 。 因為我們位於模組 BasicPlane.Figures 的主要模組介面檔案中,因此會隱含模組名稱,而且只會指定分割區名稱。

到目前為止,我們已定義主要模組介面,其會公開我們想要提供的 API 介面。 但我們只宣告了 、未定義、 area()height()width() 。 接下來,我們將藉由建立模組實作檔案來執行此動作。

建立模組單元實作檔案

模組單元實作檔案不會以 .ixx 擴充功能結尾,也就是一般 .cpp 檔案。 藉由在來源檔案的 [方案總管] 中 按一下滑鼠右鍵建立來源檔案,以新增模組單元實作檔案,選取 [新增 > 專案 ],然後選取 [C++ 檔案][.cpp]。 提供新檔案的名稱 BasicPlane.Figures-Rectangle.cpp ,然後選擇 [ 新增 ]。

模組分割實作檔案的命名慣例遵循分割區的命名慣例。 但它有副檔名 .cpp ,因為它是實作檔案。

將檔案的內容 BasicPlane.Figures-Rectangle.cpp 取代為:

module;

// global module fragment area. Put #include directives here 

module BasicPlane.Figures:Rectangle;

int area(const Rectangle& r) { return width(r) * height(r); }
int height(const Rectangle& r) { return r.ul.y - r.lr.y; }
int width(const Rectangle& r) { return r.lr.x - r.ul.x; }

此檔案的 module; 開頭是引進稱為全域模組片段 之模組 的特殊區域。 它位於具名模組的程式碼前面,您可以在其中使用預處理器指示詞,例如 #include 。 模組介面不會擁有或匯出全域模組片段中的程式碼。

當您包含標頭檔時,通常不希望將它視為模組的匯出部分。 您通常會將標頭檔當做不應該加入模組介面的實作詳細資料。 可能有想要這樣做的進階案例,但通常您不會這麼做。 全域模組片段中的指示詞不會產生個別的中繼資料( .ifc 檔案 #include )。 全域模組片段提供一個很好的位置,以包含 標頭檔,例如 windows.h 、 或 Linux unistd.h 上的 。

我們正在建置的模組實作檔案不包含任何程式庫,因為它不需要它們做為其實作的一部分。 但是,如果這樣做,這個區域是指示詞將去的地方 #include

這一行 module BasicPlane.Figures:Rectangle; 表示這個檔案是具名模組 BasicPlane.Figures 的一部分。 編譯器會自動將主要模組介面公開的類型和函式帶入此檔案。 模組實作單位在其模組宣告中的 關鍵字之前 module 沒有 export 關鍵字。

接下來是 、 height()width() 函式 area() 的定義。 它們是在 中的資料分割中 Rectangle BasicPlane.Figures-Rectangle.ixx 宣告的。 由於此模組的主要模組介面已匯入 PointRectangle 模組分割區,因此這些類型會顯示在模組單元實作檔案中。 模組實作單元的有趣功能:編譯器會自動讓檔案看到對應的模組主要介面中的所有內容。 不需要 imports <module-name>

您在實作單位內宣告的任何專案,只有它所屬的模組才能看到。

匯入模組

現在,我們將使用我們定義的模組。 開啟 ModulesTutorial.cpp 檔案。 它會自動建立為專案的一部分。 它目前包含 函式 main() 。 以下列專案取代其內容:

#include <iostream>

import BasicPlane.Figures;

int main()
{
    Rectangle r{ {1,8}, {11,3} };

    std::cout << "area: " << area(r) << '\n';
    std::cout << "width: " << width(r) << '\n';

    return 0;
}

語句 import BasicPlane.Figures; 會讓此檔案可以看到模組 BasicPlane.Figures 中的所有匯出函式和類型。 它可以在任何指示詞之前或之後 #include 出現。

然後,應用程式會使用模組中的類型和函式來輸出所定義矩形的區域和寬度:

area: 50
width: 10

模組剖析

現在讓我們更詳細地查看各種模組檔案。

主要模組介面

模組包含一或多個檔案。 其中一個定義匯入工具會看到的介面。 此檔案包含 主要模組介面 。 每個模組只能有一個主要模組介面。 如先前所述,匯出的模組介面單元不會指定模組分割區。

它預設有擴充 .ixx 功能。 不過,您可以將具有任何副檔名的來源檔案視為模組介面檔案。 若要這樣做,請將來源檔案屬性頁面的 [進階 ] 索引標籤中的 [編譯身 分] 屬性設定 [編譯為模組](/介面)

Screenshot of a hypothetical source file's Configuration properties under Configuration properties > C/C++ > Advanced > Compile As, with Compile as C++ Module Code (/interface) highlighted

模組介面定義檔的基本大綱如下:

module; // optional. Defines the beginning of the global module fragment

// #include directives go here but only apply to this file and
// aren't shared with other module implementation files.
// Macro definitions aren't visible outside this file, or to importers.
// import statements aren't allowed here. They go in the module preamble, below.

export module [module-name]; // Required. Marks the beginning of the module preamble

// import statements go here. They're available to all files that belong to the named module
// Put #includes in the global module fragment, above

// After any import statements, the module purview begins here
// Put exported functions, types, and templates here

module :private; // optional. The start of the private module partition.

// Everything after this point is visible only within this file, and isn't 
// visible to any of the other files that belong to the named module.

此檔案的開頭 module; 必須是表示全域模組片段的開頭,或是 export module [module-name]; 表示模組 purview 開頭。

模組 purview 是您要從模組公開的函式、類型、範本等位置。

您也可以透過 export import 關鍵字公開其他模組或模組分割區,如檔案所示 BasicPlane.Figures.ixx

主要介面檔案必須匯出直接或間接為模組定義的所有介面分割區,或程式格式不正確。

私人模組分割區可讓您在此檔案中只顯示想要顯示的專案。

模組介面單位在 關鍵字 前面加上 關鍵字 module export

如需深入瞭解模組語法,請參閱 模組

模組實作單位

模組實作單位屬於具名模組。 他們所屬的具名模組是由 module [module-name] 檔案中的 語句表示。 模組實作單位會提供實作詳細資料,基於程式碼衛生或其他原因,您不想要放入主要模組介面或模組資料分割檔案中。

模組實作單位適用于將大型模組分成較小的部分,這可能會導致建置時間更快。 這項技術會在最佳做法 一節中簡短 說明。

模組實作單元檔案具有 .cpp 擴充功能。 模組實作單元檔案的基本大綱如下:

// optional #include or import statements. These only apply to this file
// imports in the associated module's interface are automatically available to this file

module [module-name]; // required. Identifies which named module this implementation unit belongs to

// implementation

模組分割區檔案

模組分割區提供將模組元件化成不同片段或 分割區 的方法。 模組分割區只能匯入屬於具名模組一部分的檔案中。 無法在具名模組外部匯入它們。

分割區具有介面檔案,以及零個或多個實作檔案。 模組分割區會共用整個模組中所有宣告的擁有權。

分割區介面檔案匯出的所有名稱都必須匯入並重新匯出 export import 主要介面檔案。 分割區的名稱必須以模組名稱開頭,後面接著冒號,然後是分割區的名稱。

分割區介面檔案的基本大綱如下所示:

module; // optional. Defines the beginning of the global module fragment

// This is where #include directives go. They only apply to this file and aren't shared
// with other module implementation files.
// Macro definitions aren't visible outside of this file or to importers
// import statements aren't allowed here. They go in the module preamble, below

export module [Module-name]:[Partition name]; // Required. Marks the beginning of the module preamble

// import statements go here. 
// To access declarations in another partition, import the partition. Only use the partition name, not the module name.
// For example, import :Point;
// #include directives don't go here. The recommended place is in the global module fragment, above

// export imports statements go here

// after import, export import statements, the module purview begins
// put exported functions, types, and templates for the partition here

module :private; // optional. Everything after this point is visible only within this file, and isn't 
                         // visible to any of the other files that belong to the named module.
...

模組最佳做法

匯入它的模組和程式碼必須使用相同的編譯器選項進行編譯。

模組命名

  • 您可以在模組名稱中使用句號 ('.'),但它們對編譯器沒有特殊意義。 使用它們將意義傳達給模組的使用者。 例如,從程式庫或專案頂端命名空間開始。 使用描述模組功能的名稱來完成。 BasicPlane.Figures 意在傳達幾何平面的 API,特別是可在平面上表示的圖形。
  • 包含模組主要介面的檔案名通常是模組的名稱。 例如,假設模組名稱 BasicPlane.Figures 為 ,包含主要介面的檔案名會命名為 BasicPlane.Figures.ixx
  • 模組資料分割檔的名稱通常是 <primary-module-name>-<module-partition-name> 模組的名稱後面接著連字號 ('-'),然後是分割區的名稱。 例如,BasicPlane.Figures-Rectangle.ixx

如果您要從命令列建置,而且您會針對模組分割區使用此命名慣例,則不需要針對每個模組分割區檔案明確新增 /reference 。 編譯器會根據模組的名稱自動尋找它們。 編譯的資料分割檔名稱(以 .ifc 副檔名結尾)是從模組名稱產生。 請考慮模組名稱 BasicPlane.Figures:Rectangle :編譯器會預期 對應的 Rectangle 已編譯分割區檔案名為 BasicPlane.Figures-Rectangle.ifc 。 編譯器會使用此命名配置,藉由自動尋找分割區的介面單元檔案,更輕鬆地使用模組分割區。

您可以使用自己的慣例來命名它們。 但是,您必須將對應的 /reference 引數指定給命令列編譯器。

因素模組

使用模組實作檔案和分割區來讓您的模組更容易維護程式碼,而且編譯時間可能更快。

例如,將模組的實作移出模組介面定義檔,並移至模組實作檔案,表示實作的變更不一定會導致匯入模組的每個檔案重新編譯(除非您有 inline 實作)。

模組分割可讓您更輕鬆地以邏輯方式將大型模組分解。 它們可用來改善編譯時間,讓部分實作的變更不會重新編譯模組的所有檔案。

摘要

在本教學課程中,您已介紹 C++20 模組的基本概念。 您已建立主要模組介面、定義模組分割區,並建置模組實作檔案。

另請參閱

C++ 中的模組概觀
module、 、 import export 關鍵字
Visual Studio 中的 C++ 模組導覽
實用的 C++20 模組和 C++ 模組工具的未來
將專案移至名為 Modules 的 C++
逐步解說:在 Microsoft Visual C++ 中建置和匯入標頭單位