Im klassischen C++ besteht ein (objekt-orientiertes) Programm aus Header-Dateien, die Schnittstellen enthalten (Dateiendung .h) und Implementierungsdateien, den den dazugehörigen Programmcode enthalten (Dateiendung .cpp).
Ab C++ 20 gibt es das Sprachmittel der Module: Darunter versteht man Softwarekomponenten, die Schnittstelle und Implementation zusammenfassen (können). Sie werden unabhängig voneinander übersetzt und können danach von anderen Programmteilen verwendet werden.
Ein Modul ist eine eigene Übersetzungseinheit. Das klassische Modell der Aufteilung in Header- und Implementierungsdateien weist einige Nachteile auf:
-
Die Aufteilung in Header- und Implementierungsdatei führt zu doppelt so vielen Dateien im Vergleich dazu, wenn man Schnittstelle und Implementation beispielsweise in einer einzigen (Modul-)Datei ablegt.
-
Eine Aufteilung einer Funktionalität auf zwei Dateien kann zu Inkonsistenzen führen, wenn die Deklaration in der Header-Datei nicht mit derjenigen in der Implementierungsdatei übereinstimmt. Bei Modulen kann dieses Problem nicht auftreten.
-
Header-interne Definitionen mit
#define
sind in allen nachfolgenden Übersetzungseinheiten sichtbar. Das kann Konflikte auslösen. Module vermeiden dieses Problem. -
Das Inkludieren einer Header-Dateien wird mit Makros gesteuert (
#include
). Andere Makros wiederum überwachen, dass der Inhalt einer Header-Datei nicht zweimal berücksichtigt wird (#pragma once
). Aber auch wenn der Compiler den Inhalt ignoriert („passives Parsen”), muss er dennoch das zweite Inkludieren dieser Header-Datei bis zum Ende durchführen, um zu wissen, wann er wieder in den aktiven Modus des Übersetzens umschalten muss. Unnötige längere Compilationszeiten sind die Folge. -
Header-Dateien müssen bei jeder Übersetzung vom Compiler analysiert werden. Bei Modulen liegt das binäre Ergebnis nach deren einmaliger Übersetzung vor („precompiled” Header). Auf diese Weise wird Übersetzungszeit eingespart.
-
Es kann eine Rolle spielen, in welcher Reihenfolge Header-Dateien mit
#include
eingebunden werden. Module können in beliebiger Reihenfolge importiert werden. -
In einer Header-Datei kann es in Klassendefinitionen mit
private
gekennzeichnete Bereiche geben. Diese sind für die Benutzung einer Schnittstelle von außen nicht von Bedeutung, sogar unerwünscht. Details von privater Natur können vor dem Anwender bei Verwendung von Header-Dateien nicht wirklich versteckt werden. In Modulen sind solche Bereiche nach außen nicht sichtbar.
Obwohl dies nicht im C++ 20-Standard festgelegt ist, ermöglicht es die Erstellung eines C++ Programms mit dem Visual C++ Compiler, dass die C++–Standardbibliothek als Modul mit dem Namen „std” importiert werden kann.
Dies hat gegenüber der Vorgehensweise mit #include
–Direktiven und entsprechenden Header-Dateien
den Vorteil, dass sich die Kompilierungszeiten je nach Größe des Programms erheblich verkürzen.
Mit der Anweisung
import std;
wird die STL in die aktuelle Übersetzungseinheit importiert.
Dazu wird allerdings – in Bezug auf den Visual C++ Compiler – eine Datei std.ixx
im Programm benötigt. Diese muss man nicht selbst erstellen, sie ist in einer Visual C++ Installation
in folgendem Verzeichnis vorhanden:
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\modules\std.ixx
Update:
Ab Visual Studio 17.6.2 (möglicherweise auch einige Versionen darunter) ist das explizite
Hinzufügen der std.ixx
-Datei zum Projekt nicht mehr erforderlich!
Wenn wir den Quellcode der Datei std.ixx
betrachten, erkennen wir im nachfolgenden Ausschnitt
in Zeile 17 die Definition des Modulnamens std
:
01: // Copyright (c) Microsoft Corporation. 02: // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 03: 04: // This named module expects to be built with classic headers, not header units. 05: #define _BUILD_STD_MODULE 06: 07: module; 08: 09: // The subset of "C headers" [tab:c.headers] corresponding to 10: // the "C++ headers for C library facilities" [tab:headers.cpp.c] 11: #include 12: #include 13: ... 14: #include 15: #include 16: 17: export module std; 18: 19: #pragma warning(push) 20: #pragma warning(disable : 5244) // '#include ' in the purview of module 'std' appears erroneous. 21: 22: // "C++ library headers" [tab:headers.cpp] 23: #include 24: #if _HAS_STATIC_RTTI 25: #include 26: #endif // _HAS_STATIC_RTTI 27: #include 28: #include 29: ... 30: #include 31: #include 32: #include 33: 34: // "C++ headers for C library facilities" [tab:headers.cpp.c] 35: #include 36: #include 37: ... 38: #include 39: #include 40: 41: #pragma warning(pop)
Das nachfolgende Programmfragment (Code-Listing 1) zeigt die Einbindung eines Moduls
hello_world
mit der import
-Anweisung.
Das Modul exportiert einen Namensraum MyHelloWorld
.
Auf diese Weise stehen eine globale Variable globalData
und eine globale Funktion sayHello
zur Verfügung.
Ein Aufruf von main()
gibt
Hello Module! Data is 123
auf der Standardausgabe std::cout
aus. Dabei muss in main()
keine #include
-Anweisung
vorhanden sein: Das Modul bringt alles Notwendige mit:
01: /// Program.cpp
02: import hello_world;
03:
04: int main()
05: {
06: MyHelloWorld::globalData = 123;
07:
08: MyHelloWorld::sayHello();
09: }
Code-Listing 1: Einbindung eines Moduls.
01: /// HelloWorld.ixx
02: export module hello_world;
03:
04: import std;
05:
06: export namespace MyHelloWorld
07: {
08: int globalData{};
09:
10: void sayHello()
11: {
12: std::cout << "Hello Module! Data is " << globalData << std::endl;
13: }
14: }
Code-Listing 2: Definition/Implementierung eines Moduls.
Der Effekt des Importierens eines Moduls besteht darin, die in dem Modul deklarierten
exportierten Entitäten (hier: Namensraum MyHelloWorld
) für die importierende Übersetzungseinheit sichtbar zu machen.
Genau wie bei C++–Headerdateien müssen Module nicht zwingend aufgeteilt werden, also deren Inhalt nicht auf mehrere Dateien verteilt werden. Trotzdem können große Quelldateien unhandlich werden, sodass C++–Module auch eine Möglichkeit bieten, ein einzelnes Modul in verschiedene Übersetzungseinheiten zu unterteilen. Diese Unterteilungen werden als Partitionen bezeichnet.
Eine Partition wiederum kann aus einer oder aus mehreren Dateien bestehen. Hierbei unterscheidet man Module Interface Units und Module Implementation Units:
Eine Module Interface Unit kann es nur einmal geben, die dazugehörigen Module Implementation Units wiederum lassen sich auf mehrere Dateien aufteilen:
01: /// Program.cpp
02: import modern_cpp;
03:
04: import std;
05:
06: int main()
07: {
08: main_shared_ptr();
09: main_unique_ptr();
10: main_weak_pointer();
11: ...
12: return 0;
13: }
Code-Listing 3: Hauptprogramm.
/// Module_Modern_Cpp.ixx
export module modern_cpp;
export import :shared_ptr;
export import :unique_ptr;
export import :weak_ptr;
....
Code-Listing 4: Primäre Modulschnittstelle: Modul modern_cpp
.
01: /// Module Interface Partition: File Module_SharedPtr.ixx
02: export module modern_cpp:shared_ptr;
03:
04: import std;
05:
06: export void main_shared_ptr();
Code-Listing 5: Modulschnittstellenpartition / „Module Interface Partition”: Partition :shared_ptr
.
01: /// Module Implementation Partition: File SharedPtr.cpp
02: module modern_cpp:shared_ptr;
03:
04: namespace SharedPointer {
05:
06: void test_01_shared_ptr ()
07: { ...
08: }
09:
10: void test_02_shared_ptr ()
11: { ...
12: }
13:
14: void test_03_shared_ptr ()
15: { ...
16: }
17: }
18:
19: void main_shared_ptr()
20: {
21: using namespace SharedPointer;
22: test_01_shared_ptr ();
23: test_02_shared_ptr ();
24: test_03_shared_ptr ();
25: }
Code-Listing 6: Modulimplementierungspartition / „Module Implementation Partition”: Partition :shared_ptr
.
Die Anregungen zu diesem Code-Snippet finden sich teilweise in
Understanding C++ Modules: Part 1: Hello Modules, and Module Units
(abgerufen am 22.04.2023)
vor. Eine andere, empfehlenswerte Beschreibung stammt von Simon Toth:
C++20 Modules – Complete Guide
(abgerufen am 22.04.2023)