C++20: Module Interface Unit und Module Implementation Unit
by Rainer GrimmDank Module Interface Unit und Module Implementation Unit lässt sich die Definition eines Moduls in sein Interface und seine Implementierung aufteilen. Der heutige Artikel zeigt, wie sich das umsetzen lässt.
Wie ich in meinem letzten Artikel "C++20: Ein einfaches math-Modul" angekündigt habe, werde ich diesen Artikel mit meiner Clang-Odyssee beginnen. Mein kleiner Umweg ist zugleich eine kompakte Wiederholung meines letzten Artikels.
Meine Clang-Odyssee
Aufgrund der Vorträge von Boris Kolpackov "Building C++ Modules" auf der CppCon 2017 oder Corentin Jabot "Modules are not a tooling opportunity" hatte ich geglaubt, dass die Compiler-Hersteller die folgenden Suffixe für Module vorschlagen:
- Windows: ixx
- Clang: cppm
- GCC: kein Suffix
Im Fall des Clang-Compilers lag ich daneben. Das ist das einfache math
-Modul, das ich mit dem Clang-Compiler übersetzen wollte:
// math.cppm
export module math;
export int add(int fir, int sec){
return fir + sec;
}
Ich verwendete zum Übersetzen den Clang-9- und den Clang-10-Compiler auf Microsoft und Linux. Darüber hinaus baute ich den aktuellen Clang 11 direkt aus den Quellen und setzte ihn ein. Alle meine Versuche, das Modul zu erzeugen, endeten mit einer ähnlichen Fehlermeldung:
Die Kommandozeile sollte das Modul math.pcm
erzeugen. Ich verwendete die Kommandozeile -std=c++20 -fmodules-ts
, aber die Fehlermeldung ergab: module interface compilation requires '-std=c++20' or '-fmodules-ts'
. Ich spielte alle Variationen der beiden Flags durch, fügte das globale Modul-Fragment der Modul-Definition hinzu und versuchte es noch mit weiteren Flags. Das Ergebnis war immer dasselbe.
Dann bat ich Arthur O'Dwyer und Roland Bock um ihre Hilfe. Arthur hatte bereits Module mit dem Clang erzeugt: "Hello World with C++2a modules". Roland hatte wie ich den Clang 11 direkt aus den Quellen gebaut und mein math
-Modul direkt erzeugen können. Wir beiden hatten buchstäblich denselben Clang-Compiler und dieselbe Moduldefinition verwendet. Buchstabe für Buchstabe verglich ich seine Kommandozeile mit meiner. Da fiel mir der entscheidende Unterschied auf:
Meine: clang++ -std=c++20 - -fmodules-ts -stdlib=libc++ -c math.cppm -Xclang -emit-module-interface -o math.pcm
Roland: clang++ -std=c++20 - -fmodules-ts -stdlib=libc++ -c math.cpp -Xclang -emit-module-interface -o math.pcm
Roland nannte seine Moduldefinition math.cpp
. cpp
war genau das Suffix, das auch Arthur eingesetzt hatte. Gib deinem Modul nicht das Suffix cppm.
Jetzt waren das Erzeugen und das Verwenden des Moduls ein Kinderspiel:
Um diesen Exkurs zu beenden, stelle ich kurz noch die client.cpp
-Datei vor und sage noch ein paar Worte zu den Flags für den Clang-Compiler:
// client.cpp
import math;
int main() {
add(2000, 20);
}
clang++ -std=c++2a -stdlib=libc++ -c math.cpp -Xclang -emit-module-interface -o math.pcm // (1)
clang++ -std=c++2a -stdlib=libc++ -fprebuilt-module-path=. client.cpp math.pcm -o client // (2)
- Erzeugt das Modul
math.pcm
. Das Suffixpcm
steht für ein vorkompiliertes Modul (precompiled modul). Die Kombination der Flags-Xclang -emit-module-interface
ist für die Erzeugung des vorkompilierten Moduls notwendig. - Erzeugt die ausführbare Datei
client
, die das Modulmath.pcm
verwendet. Dazu musst du den Pfad zu dem Modul mit dem Flag-fprebuilt-module-path
angeben.
Das Modul math
war sehr einfach gestrickt. Das nächste Modul soll ein wenig anspruchsvoller werden.
Regel für die Struktur eines Moduls
Hier ist die erste Regel für die Struktur eines Moduls:
module; // global module fragment
#include <headers for libraries not modularized so far>
export module math; // module declartion
import <importing of other modules>
<non-exported declarations> // names with only visibiliy inside the module
export namespace math {
<exported declarations> // exported names
}
Diese Regel hilft in doppelter Hinsicht. Sie gibt dir eine einfache Struktur für ein Modul vor und eine Idee, worüber ich noch schreiben werde. Was ist daher neu in der Struktur des Moduls?
- In Module lassen sich andere Module importieren. Sie haben Modulbindung und sind nicht außerhalb des Moduls sichtbar. Diese Beobachtung gilt auch für die nichtexportierten Deklarationen (non-exported declarations).
- Ich habe die exportierten Namen in einen Namensraum
math
verpackt. Er besitzt denselben Namen wie das Modul. - Die Namen in dem Modul sind lediglich deklariert und nicht definiert. Nun möchte ich auf die Trennung des Interfaces und der Implementierung eines Moduls eingehen.
Module Interface Unit und Module Implementation Unit
Entsprechend der Regel zum Aufbau eines Moduls möchte ich das Modul math
des letzten Artikels "C++20: Ein einfaches math-Modul" refaktorieren.
Module Interface Unit
// mathInterfaceUnit.ixx
module;
import std.core;
export module math;
export namespace math {
int add(int fir, int sec);
int getProduct(const std::vector<int>& vec);
}
- Die Module Interface Unit enthält die exportierte Modul-Deklaration:
export module math
. - Die Namen
add
undgetProduct
werden exportiert. - Ein Modul kann nur eine Module Interface Unit besitzen.
Module Implementation Unit
// mathImplementationUnit.cpp
module math;
import std.core;
int add(int fir, int sec){
return fir + sec;
}
int getProduct(const std::vector<int>& vec) {
return std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>());
}
- Die Module Implementation Unit enthält die nicht exportierte Moduldeklaration:
module math
. - Ein Modul kann mehrere Module Implementation Units besitzen.
Das main-Programm
// client3.cpp
import std.core;
import math;
int main() {
std::cout << std::endl;
std::cout << "math::add(2000, 20): " << math::add(2000, 20) << std::endl;
std::vector<int> myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << "math::getProduct(myVec): " << math::getProduct(myVec) << std::endl;
std::cout << std::endl;
}
- Für Anwender ändert sich nicht viel. Sie müssen lediglich den Namensraum
math
verwenden.
Bauen der ausführbaren Datei
Das händische Bauen der ausführbaren Datei besteht aus ein paar Schritten:
cl.exe /std:c++latest /c /experimental:module mathInterface.ixx /EHsc /MD // (1)
cl.exe /std:c++latest /c /experimental:module mathImplementationUnit.cpp /EHsc /MD // (2)
cl.exe /std:c++latest /c /experimental:module client3.cpp /EHsc /MD // (3)
cl.exe client3.obj mathInterfaceUnit.obj mathImplementationUnit.obj // (4)
- Erzeugt die Objektdatei
mathInterfaceUnit.obj
und die Module-Interface-Dateimath.ifc.
- Erzeugt die Objektdatei
mathImplementationUnit.obj.
- Erzeugt die Objektdatei
client3.obj.
- Erzeugt die Objektdatei
client3.exe.
Für den Microsoft-Compiler müssen das Exception Handling (/EHsc) und die Multithreading-Bibliothek (/MD
) verwendet werden. Darüber hinaus ist das Flag /std:c++latest
notwendig.
Zu guter Letzt ist hier die Ausgabe des Programms:
Anzeige
Wie geht's weiter?
In meinem nächsten Artikel werde ich das Modul math
um weitere Features erweitern. Zuerst werde ich Module in ein Modul importieren und die importierten Module als Bestandteil eines neuen Moduls anbieten; darüber hinaus wird das nächste Modul Namen enthalten, die nur innerhalb des Moduls sichtbar sind.