Inhaltsverzeichnis
Module
Abschied vom Präprozessor?
Programme bestehen aus mehreren Übersetzungseinheiten, abgesehen von Ausnahmen:
#include <iostream> int main() { std::cout << "Hallo, Welt\n"; }
Das Einbinden anderer Quellen per #include
verlangsamt den Übersetzungsprozess. Umfangreiche Header müssen immer wieder mit übersetzt werden. Manche Compiler behelfen sich mit Vorübersetzung (precompiled headers). Reihenfolge- und zyklische Abhängigkeiten (#ifdef
und #pragma once
), Verletzungen der „one definition rule“, ungenügende Abschirmung von Implementierungsdetails (Pimpl-Idiom, namespace xy::detail
) und vor unbeabsichtigten Wirkungen von Makros (__Uglified_Names
in Bibliotheken) sind negative Folgen dieser Technik. Module ändern ab C++20 die Organisation von Quelltexten, den Übersetzungsprozess und die Arbeit von Werkzeugen (Build-Systeme, Entwicklungsumgebungen) tiefgreifend. Der Umstellungsprozess wird aber Jahre dauern.
Module importieren
Übersetzungseinheiten können Module importieren und nutzen:
import hello; int main() { greeting(); }
Importierbar sind (übergangsweise?) auch C++-Header wie <iostream>
. Die Standardbibliothek wurde in C++20 noch nicht modularisiert. Modulnamen, die mit std
, evtl. gefolgt von Ziffern, beginnen, sind für die Standardbibliothek reserviert. Modulnamen können durch Punkte unterteilt werden: std.core
und std.core.algorithm
wären trotz gleicher Namensteile voneinander unabhängige Module.
Schnittstelle
Ein Modul besteht aus genau einer Modul-Schnittstellen-Datei und null bis vielen Implementierungsdateien. Kennzeichen einer Modulschnittstelle ist die Anweisung export module modulname;
export module hello; export void greeting();
In der Schnittstelle werden alle zum Modul gehörenden und vom Modul exportierbaren Namen aufgeführt. Das Schlüsselwort export
steht vor einzelnen Deklarationen, vor Klassen, Funktionen, Schablonen, Namensräumen, oder als Block export {...}
. Makros werden nicht exportiert.
Implementierung
Funktionen werden in der Modulschnittstelle oder anderen zum Modul gehörenden Dateien definiert:
module hello; import <iostream>; void greeting() { std::cout < "Hello, world\n"; }
Die Zugehörigkeit zu einem Modul wird durch module modulname;
festgelegt. Dabei kann auf alle in der Schnittstelle aufgeführten Namen zurückgegriffen werden. Modulname und import
-Anweisungen stehen vor allem anderen Deklarationen eines Moduls.
Reexportieren
export module noodles; export import spaghetti; ...
erlaubt, die Schnittstellen anderer Module mit zu exportieren.
Partitionen
export module geometry:vector; ...
Modulpartitionen, erkennbar am Doppelpunkt im Namen, können nur von dem übergeordneten Modul importiert und auch reexportiert werden:
export module geometry; export import :vector;
Auch Modulpartitionen können neben der Schnittstelle weitere Implementierungsdateien besitzen.
Globales und privates Fragment
Eine Modul(schnittstellen)datei hat zwingend folgende Struktur (Teile in Eckigen Klammern können wegfallen):
[ // nur Kommentare module; // globales Modulfragment ... ] // nur Präprozessoranweisungen [export] modul modulname; // Modulpräambel [export][import andere;] // Importbereich ... // Modulbereich [module :private; // privates Modulefragment ... ]
Eine Modulschnittstelle mit privatem Modulbereich darf keine weiteren Implementierungsdateien haben.
Modul-Bindung
Neben lokaler und externer Bindung führt das Modulsystem eine weitere Bindungsart ein. Innerhalb von Modulschnittstellen definierte Variablen sind innerhalb aller Übersetzungseinheiten des Moduls nutzbar. Mit anderen externen Variablen kollidieren sie nicht, wenn sie nicht exportiert werden, wie dieses Beispiel mit mehreren Modulpartitionen zeigt:
export module walk:position; int x = 0;
export module walk:steps; import :position; export void forward() { ++x; } export void back() { --x; }
export module walk; export import :steps; import :position; export auto get_position() { return x; }
// does not collide with x in module walk: int x = 7;
import walk; import std.core; // Visual C++ 2019: instead of // import <iostream>; int main() { forward(); forward(); back(); std::cout << get_position() << " steps gone\n"; extern int x; std::cout << x << " is another, unaffected global variable\n"; }
Übersetzungsprozess
Modulschnittstellen als auch Implementierungsdateien sind einzelne Übersetzungseinheiten, die beim Übersetzen zu Ohjektdateien (*.o
, *.obj
) werden. Bevor Module importiert werden können, müssen die Modulschnittstellen bekannt sein. Dazu müssen die Schnittstellen zuerst, in der Reihenfolge ihrer Abhängigkeiten, verarbeitet werden. Zyklische Abhängigkeiten sind nicht erlaubt. Die Art, wie Compiler die Informationen dieser Schnittstellen aufbereiten, vorhalten und wiederfinden, z.B. als binary module interface (*.bmi
, *.ifc
), ist implementierungsabhängig. Hier kommt noch viel Arbeit auf die Hersteller von Buildsystemen zu.
Übersetzungseinheit | Dateiendung1) | erzeugt | |
---|---|---|---|
Modul-Schnittstelle | export module name; | *.cppm / *.ixx | .o und *.bmi / *.ifc |
Importierbare Header | import <header>; | ||
import “header”; | *.hpp / *.h | .o und *.bmi / *.ifc |
|
Modul-Implementierung | module name; | *.cpp | .o |
Konsument | import name; | *.cpp | .o |
*.ixx -> Prepocess/Compile -> *.obj Libraries v \ / *.ifc Link -> program v / *.cpp -> Compile -> *.obj