C++20-Module: Private Module Fragment und Header Units
In den letzten Wochen habe ich etwas Neues zu Modulen in C++20 gelernt: Private Module Fragment und Header Units. Deshalb mache ich in diesem Beitrag einen kleinen Umweg über Module und stelle deren neue Funktionalität vor.
- Rainer Grimm
In den letzten Wochen habe ich etwas Neues zu Modulen in C++20 gelernt: private
Module Fragment und Header Units. Deshalb mache ich in diesem Beitrag einen kleinen Umweg über Module und stelle deren neue Funktionalität vor.
Du fragst dich vielleicht, warum ich meinen angekündigten Beitrag über Variadic Templates verschiebe? Der Grund ist einfach. Mein kommendes pdf-Bundle, das ich nächste Woche veröffentliche, beinhaltet die C++20-Module und ich möchte diesen Beitrag in das Bundle einbinden, daher ziehe ich dessen Veröffentlichung vor.
Ein private
Module Fragment und Header Units machen den Umgang mit Modulen in C++20 deutlich komfortabler.
Ich verwende in diesem Beitrag absichtlich den neuesten Visual Studio Compiler, denn er unterstützt C++20-Module fast vollständig. Die neuesten GCC und Clang Compiler unterstützen Module hingegen nur teilweise.
private
Module Fragment
Ich bin mir nicht sicher, ob du die Fakten über die Module Interface Unit und die Module Implementation Unit parat hast? Deshalb möchte ich die wichtigsten wiederholen.
Wenn du das Modul in ein Interface und dessen Implementierung separieren willst, bietet es sich an, es in eine Module Interface Unit und eine oder mehrere Module Implementation Units zu unterteilen.
Module Interface Unit
// mathInterfaceUnit2.ixx
module;
#include <vector>
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 exportierende Moduldeklaration:
export module math
. - Die Namen
add
undgetProduct
werden exportiert. - Ein Modul kann nur eine Module Interface Unit besitzen.
Modul Implementation Unit
// mathImplementationUnit2.cpp
module math;
#include <numeric>
namespace math {
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 nicht-exportierende Moduldeklarationen:
module math;.
- Ein Modul kann mehr als eine Module Implementation Unit besitzen.
Das main
-Programm
// client4.cpp
#include <iostream>
#include <vector>
import math;
int main() {
std::cout << '\n';
std::cout << "math::add(2000, 20): " << math::add(2000, 20) << '\n';
std::vector<int> myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << "math::getProduct(myVec): " << math::getProduct(myVec) << '\n';
std::cout << '\n';
}
Aus der Sicht des Benutzers wurde nur der Namensraum math
hinzugefĂĽgt.
Erstellen der ausfĂĽhrbaren Datei
Das manuelle Erstellen der ausfĂĽhrbaren Datei umfasst folgende Schritte.
cl.exe /std:c++latest /c mathInterfaceUnit2.ixx /EHsc // (1)
cl.exe /std:c++latest /c mathImplementationUnit2.cpp /EHsc // (2)
cl.exe /std:c++latest /c client4.cpp /EHsc // (3)
cl.exe client4.obj mathInterfaceUnit2.obj mathImplementationUnit2.obj // (4)
- Erzeugt die Objektdatei
mathInterfaceUnit2.obj
und die Modulschnittstellendateimath.ifc
. - Erzeugt die Objektdatei
mathImplementationUnit2.obj
. - Erzeugt die Objektdatei
client4.obj
. - Erzeugt die ausfĂĽhrbare Datei
client4.exe
.
FĂĽr den Microsoft-Compiler muss das Modell fĂĽr die Ausnahmebehandlung (/EHsc
) zusätzlich angeben werden. Verwende außerdem das Flag /std:c++latest
.
Zum Abschluss liefert das Programm dann diese Ausgabe:
Einer der groĂźen Vorteile der Strukturierung von Modulen in eine Module Interface Unit und eine oder mehrere Module Implementation Units ist, dass sich Ă„nderungen in den Module Implementation Units nicht auf die Module Interface Unit auswirken und daher keine Neukompilierung erforderlich ist.
private
Module Fragment
Dank eines private
Module Fragment kannst du ein Modul in einer Datei implementieren und seinen letzten Teil mit module :private;
als seine Implementierung deklarieren. Folglich fĂĽhrt eine Ă„nderung des privaten Modulfragments nicht zu einer Neukompilierung des Moduls. Die folgende Moduldeklarationsdatei mathInterfaceUnit3.ixx
fasst die Module Interface Unit mathInterfaceUnit2.ixx
und die Module Implementation Unit mathImplementationUnit2.cpp
in einer Datei zusammen.
// mathInterfaceUnit3.ixx
module;
#include <numeric>
#include <vector>
export module math;
export namespace math {
int add(int fir, int sec);
int getProduct(const std::vector<int>& vec);
}
module :private; // (1)
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>());
}
module: private;
(Zeile 1) kennzeichnet den Beginn des private
Module Fragments. Eine Ă„nderung in diesem optionalen letzten Teil einer Moduldeklarationsdatei fĂĽhrt nicht dazu, dass sie neu kompiliert wird.
Ich habe die Header Units bereits in einem frĂĽheren Beitrag vorgestellt. Jetzt kann ich sie einsetzen.
Header-Units
Header Units sind ein einfacher Weg, um von Headern auf Module umzustellen. Du musst lediglich die #include
-Direktive durch die neue import
-Anweisung ersetzen.
#include <vector> => import <vector>;
#include "myHead.h" => import "myHead.h";
Erstens wendet import
die gleichen Regeln wie include
an, um Namen aufzulösen. Das bedeutet im Fall der Anführungszeichen ("myHeader.h
"), dass das Lookup zuerst im lokalen Verzeichnis sucht, bevor es mit dem Systemsuchpfad fortfährt.
Zweitens ist dies weit mehr als eine Textersetzung. In diesem Fall erzeugt der Compiler aus der import
-Anweisung etwas Modul-ähnliches und behandelt das Ergebnis so, als ob es ein Modul wäre. Die importierende Modulanweisung erhält alle exportierbaren Namen des Headers. Zu den exportierbaren Namen gehören auch Makros. Der Import dieser synthetischen Header-Einheiten ist schneller und in der Geschwindigkeit mit vorkompilierten Headern vergleichbar.
Module sind keine vorkompilierten Header
Vorkompilierte Header sind eine nicht standardisierte Methode, um Header in einer Zwischenform zu kompilieren, die vom Compiler schneller verarbeitet werden kann. Der Microsoft-Compiler verwendet die Erweiterung .pch
und der GCC-Compiler .gch
für vorkompilierte Header. Der Hauptunterschied zwischen vorkompilierten Headern und Modulen ist, dass Module selektiv Namen exportieren können. Nur in einem Modul exportierte Namen sind außerhalb des Moduls sichtbar.
Nach dieser kurzen Auffrischung möchte ich Header Units gerne einsetzen.
Verwenden von Header Units
Das folgende Beispiel besteht aus drei Dateien. Die Header-Datei head.h
, in der die Funktion hello
deklariert wird, ihre Implementierungsdatei head.cpp
und die Client-Datei helloWorld3.cpp
, die die Funktion hello
verwendet.
// head.h
#include <iostream>
void hello();
Nur die Implementierungsdatei head.cpp
und die Client-Datei helloWorld3.cpp
sind besonders. Sie importieren die Header-Datei head.h: import "head.h";
.
// head.cpp
import "head.h";
void hello() {
std::cout << '\n';
std::cout << "Hello World: header units\n";
std::cout << '\n';
}
// helloWorld3.cpp
import "head.h";
int main() {
hello();
}
Die nachfolgenden Schritte sind notwendig, um Header Units zu verwenden.
cl.exe /std:c++latest /EHsc /exportHeader head.h
cl.exe /c /std:c++latest /EHsc /headerUnit head.h=head.h.ifc head.cpp
cl.exe /std:c++latest /EHsc /headerUnit head.h=head.h.ifc helloWorld3.cpp head.obj
- Das Flag
/exporHeader
(erste Zeile) bewirkt, dass die ifc-Dateihead.h.ifc
aus der Header-Dateihead.h
erstellt wird. Die ifc-Datei enthält die Metadatenbeschreibung der Modulschnittstelle. - Die Implementierungsdatei
head.cpp
(zweite Zeile) und die Client-DateihelloWordl3.cpp
(dritte Zeile) verwenden die Header-Unit. Das Flag/headerUnit head.h=head.h.ifc
importiert den Header und teilt dem Compiler oder Linker den Namen der ifc-Datei fĂĽr den angegebenen Header mit.
Einen Nachteil haben Header Units aber doch: Nicht alle Header sind importierbar. Welche Header importierbar sind, hängt von der Implementierung ab. Der C++-Standard garantiert, dass alle Header der Standardbibliothek importierbar sind. Die Möglichkeit zu importieren, schließt C-Header aus.
Wie geht's weiter?
In meinem nächsten Beitrag verwende ich Variadic Templates, um das C++-Idiom für eine vollständig generische Fabrik zu implementieren. Eine Implementierung dieses wichtigen C++-Idioms ist std::make_unique
.
()