C++20: Modules

Modernes C++  –  6 Kommentare

Module sind eines der fünf großen Features von C++20. Sie sollen die Einschränkungen von Header-Dateien überwinden. Sie versprechen aber noch mehr.

Mit Modulen soll die Trennung von Header-Dateien und Implementierungsdateien genauso der Vergangenheit angehören wie der Präprozessor. Letztlich soll das Übersetzen der Software deutlich schneller werden und es soll einfacher werden, Pakete zu schnüren.

Module aus der Perspektive des Anwenders vorzustellen, ist relativ einfach. Das gilt aber nicht für die Perspektive des Implementierers. Mein Plan für den heutigen Artikel ist es, mit einem einfachen Programm zu beginnen und sukzessive mehr Features hinzuzufügen.

Ein erstes Beispiel

Hier ist bereits das erste Modul math:

// math.cppm

export module math;

export int add(int fir, int sec){
return fir + sec;
}

Der Ausdruck export module math steht für die Moduldeklaration. Dank des Schlüsselworts export vor der Funktion add wird diese Funktion exportiert und kann damit von einem Anwender verwendet werden:

// main.cpp

import math;

int main(){

add(2000, 20);

}

import math importiert das Modul math und bewirkt, dass die exportierten Namen in der Datei main.cpp sichtbar werden. Das war der einfache Teil meiner Aufgabe. Die Herausforderung beginnt dann, wenn ich das Programm übersetze.

Die Modul-Deklarationsdatei

Ist dir der seltsame Namen math.cppm des Modules aufgefallen?

  • Das Suffix cppm steht wohl für cpp-Moduldeklaration und ist das Akronym, das Clang verwendet.
  • cl.exe verwende das Suffix ixx. Ich nehme an, in diesem Fall steht das i für Interface.
  • Für den GCC kenne ich kein Suffix.
Das Modul math übersetzen

Um das Modul zu übersetzen, solltest du einen sehr aktuellen Clang order cl.exe-Compiler verwenden. Es ist auch möglich, den GCC einzusetzen. In meinen Artikel werde ich mich aber auf den Clang- und cl.exe-Compiler beschränken. Hier sind mehr Details zu meinen Compilern:

  • Clang
  • cl.exe

Genau hier ist der Punkt, an dem der Spaß losging: Die Kommandozeile für Clang++ und cl.exe zu bestimmen:

clang++ -std=c++2a -fmodules-ts --precompile math.cppm -o math.pcm                   // 1
clang++ -std=c++2a -fmodules-ts -c math.pcm -o math.o // 2
clang++ -std=c++2a -fmodules-ts -fprebuilt-module-path=. math.o main.cpp -o math // 3


cl.exe /std:c++latest /experimental:module /TP /EHsc /MD /c math.cppm /module:interface /Fo: math.obj /module:output math.pcm // 1
cl.exe /std:c++latest /experimental:module /TP /EHsc /MD /c main.cpp /module:reference math.pcm /Fo: main.obj // 2
cl.exe math.obj main.obj // 3
  1. Erzeugt aus der Moduledeklaration math.cppm ein vorcompiliertes Modul math.pcm.
  2. Erzeugt eine Übersetzungseinheit math.o, die kein Modul darstellt.
  3. Erzeugt eine ausführbare Datei math oder math.exe. clang++ benötigt hier noch den Pfad zum Modul.

Naheliegenderweise zeige ich nicht die Ausgabe des Programms. Das hole ich später nach, wenn es etwas zu zeigen gibt.

Aus Sicht des Implementierers kann die Moduldefinition in ein Interface und eine Implementierung separiert werden. Bevor ich auf diesen Punkt genauer eingehen, möchte ich erst einen Schritt rückwärts gehen und die folgende Frage beantworten.

Was sind die Vorteile von Modulen?

  • Schnellere Übersetzungszeiten: Ein Modul wird nur einmal importiert. Dies sollte buchstäblich nichts kosten. Vergleiche dies mit M Header-Dateien, die in N Übersetzungseinheiten inkludiert werden. Die kombinatorische Explosion bedeutet in diesem Fall, dass M * N-mal ein Header evaluiert werden muss.
  • Isolation von Makros: Falls es einen Konsens in der C++-Community gibt, dann der, dass wir die Präprozessor-Makros loswerden sollten. Warum? Die Verwendung eines Makros ist nur Textersetzung ohne Verständnis der C++-Semantik. Das hat viele negative Konsequenzen. Zum Beispiel kann das Verhalten davon abhängig sein, in welcher Reihenfolge Makros inkludiert werden oder Makros können mit bereits definierten Makros oder Namen deiner Applikation kollidieren. Im Gegensatz dazu ist es unerheblich, in welcher Reihenfolge Module importiert werden.
  • Ausdruck der logischen Struktur deines Codes: Module erlauben es auszudrücken, welcher Name exportiert oder nicht exportiert werden soll. Du kannst auch ein paar Module in einem Modul verpacken und sie deinem Kunde als ein logisches Paket anbieten.
  • Keine Header-Dateien notwendig: Es gibt keine Notwendigkeit mehr, deine Quelldateien in Interface- und Implementierungseinheiten zu trennen. Das bedeutet, dank Modulen wird die Anzahl der Quelldateien halbiert.
  • Hässliche Workarounds loswerden: Wir haben uns bereits an hässliche Workarounds wie "verpacke deine Header-Datei in einen Include-Guard" oder "schreibe Makros mit LONG_UPPERCASE_NAMES" gewöhnt. Im Gegensatz dazu führen identische Namen in Modulen zu keiner Namenskollision.

Mein erstes Modul math habe ich in einem Modul math.cppm definiert. Es gibt neue Einheiten für Module.

Modul-Interface-Einheit und Modulimplementierungseinheit

Zuerst mal besteht das neue Modul math1 aus einer Modul-Interface-Einheit und einer Modulimplementierungseinheit.

Modul-Interface-Einheit
// math1.cppm

export module math1;

export int add(int fir, int sec);
  • Die Modul-Interace-Einheit enthält die exportierende Moduldeklaration: export module math1.
  • Namen wie add können nur in der Modul-Interface-Einheit exportiert werden.
  • Namen, die nicht exportiert werden, sind außerhalb des Moduls nicht sichtbar. Auf diesen Punkt werde ich noch genauer in meinem nächsten Artikel eingehen.
  • Ein Modul kann nur eine Modul-Interface-Einheit besitzen.
Modulimplementierungseinheit
// math1.cpp

module math1;

int add(int fir, int sec){
return fir + sec;
}
  • Die Modulimplementierungseinheit enthält die nicht exportierende Moduldeklaration: module math1;
  • Ein Modul kann mehr als eine Modulimplementierungseinheit besitzen.
main-Programm
// main1.cpp

import math1;

int main(){

add(2000, 20);

}
  • Aus der Sicht des Anwenders hat sich nichts durch das neue Modul geändert. Lediglich der Name des Moduls ist nun math1.

Das Übersetzen des Programms ist wieder deutlich anspruchsvoller.

Übersetzen des Moduls math1
clang++ -std=c++2a -fmodules-ts --precompile math1.cppm -o math1.pcm               // 1
clang++ -std=c++2a -fmodules-ts -c math1.pcm -o math1.pcm.o // 2
clang++ -std=c++2a -fmodules-ts -c math1.cpp -fmodule-file=math1.pcm -o math1.o // 2
clang++ -std=c++2a -fmodules-ts -c main1.cpp -fmodule-file=math1.pcm -o main1.o // 3
clang++ math1.pcm main1.o math1.o -o math // 4

cl.exe /std:c++latest /experimental:module /TP /EHsc /MD /c math1.cppm /module:interface /Fo: math1.pcm.obj /module:output math1.pcm // 1
cl.exe /std:c++latest /experimental:module /TP /EHsc /MD /c math1.cpp /module:reference math1.pcm /Fo: math1.obj // 2
cl.exe /std:c++latest /experimental:module /TP /EHsc /MD /c main1.cpp /module:reference math1.pcm /Fo: main1.obj // 3
cl.exe math1.obj main1.obj math1.pcm.obj // 4
  1. Erzeugt aus der Moduldeklaration math1.cppm ein vorkompiliertes Modul math1.pcm.
  2. Übersetzt das vorkompilierte Modul math1.pcm. Übersetzt die Quelldatei math1.cpp: math1.o. cl.exe führt beides in einem Schritt aus.
  3. Übersetzt das main-Programm: math1.o oder math.obj.
  4. Erzeugt eine ausführbare Datei math1 oder math1.exe.

Wie geht's weiter?

Wie versprochen, stellt dieser Artikel nur die Einleitung zu Modulen dar. In meinem nächsten Artikel werde ich mehr in die Untiefen abtauen. Insbesondere werde ich die Ausgabe des Programms darstellen. Dazu muss ich aber die Header-Datei <iostream> inkludieren oder das Modul std.core importieren.

C++-Schulungen im Großraum Stuttgart

Ich freue mich darauf, weitere C++-Schulungen halten zu dürfen.

Die Details zu meinen C++- und Python-Schulungen gibt es auf www.ModernesCpp.de.