C++20: Weitere Details zur Modulunterstützung der großen drei Compiler
Im Vergleich zeigt sich, wie sich die großen drei Compiler GCC, Clang und der Microsoft-Compiler im Zusammenspiel mit Modulen schlagen.
- Rainer Grimm
In meinem letzten Artikel, "C++20: Modulunterstützung der großen drei Compiler", habe ich ein einfaches Modul mit den Großen Drei kompiliert. Heute gehe ich tiefer und verwende den GCC, Clang und den Microsoft Compiler.
Dieser Artikel ist ziemlich technisch und endet mit einer kuriosen Beobachtung. Darüber hinaus setzt er grundlegende Kenntnisse über Module voraus, das sich in den vorherigen Artikeln über Module findet:
- Die Vorteile von Modulen
- Ein einfaches math Modul
- Module Interface Unit und Module Implementation Unit
- Module strukturieren
- Weitere offene Fragen zu Modulen
- Private Module Fragment und Header Units
Ich verwende Microsofts cl.exe 19.29.20133 für x64, den Clang 16.0.5 und den GCC 11.1.0 Compiler für meine Experimente.
Microsoft Visual Compiler
Beginnen wir mit dem Microsoft Compiler.
Der Microsoft Visual Compiler bietet verschiedene Optionen für die Verwendung von Modulen an:
Außerdem gibt es ein paar allgemeine cl.exe
Compiler-Optionen:
In den folgenden Befehlszeilen verwende ich verschiedene Compiler-Optionen für die ifc-Datei. Die ifc-Datei ist das Modul und enthält die Metadatenbeschreibung der Modulschnittstelle.
- Verwende das Modul
math.cppm
, um die obj- und ifc-Datei zu erstellen.
cl.exe /c /std:c++latest /interface /TP math.cppm
- Verwende das Modul
math.cppm
, um nur die ifc-Datei zu erstellen.
cl.exe /c /std:c++latest /ifcOnly /interface /TP math.cppm
- Verwende das Modul
math.cppm
, um die obj-Dateimath.obj
und die ifc-Dateimathematic.ifc
zu erstellen.
cl.exe /c /std:c++latest /interface /TP math.cppm /ifcOutput mathematic.ifc
- Erzeuge die ausführbare Datei
client.exe
und verwende dabei die ifc-Dateimath.inter
.
cl.exe /std:c++latest client.cpp math.obj /reference math.inter
- Erzeugt die ausführbare Datei
client.exe
und verwendet explizit die ifc-Dateimath.inter
, die sich im VerzeichnisifcFiles
befindet.
cl.exe /std:c++latest client.cpp math.obj /ifcSearchDir ifcFiles /reference math.inter
Nun kommt der Clang-Compiler zum Einsatz.
Clang-Compiler
Der Clang-Compiler bietet verschiedene Optionen für die Erstellung von Modulen an:
Weitere Einzelheiten finden sich in der offiziellen Dokumentation zu Standard C++ Modules. In den folgenden Befehlszeilen verwende ich die Compiler-Optionen für das Modul und die ifc-Datei.
- Verwende die Moduldeklarationsdatei
math.cppm
, um die pcm-Dateimath.pcm
zu erstellen.
clang++ -c -std=c++20 -fmodule-output math.cppm -o math.pcm
Verwende das Modul mit der Erweiterung ixx (math.ix
x), um die pcm-Datei math.pcm
zu erstellen.
clang++ -std=c++20 --precompile -x c++-module math.ixx -o math.pcm
Erstelle die pcm-Datei und verwende sie.
clang++ -std=c++20 -c math.pcm -o math.o
clang++ -std=c++20 -fprebuilt-module-path=. math.o client.cpp -o client.exe
Verwende die pcm-Datei other.pcm
und kompiliere sie.
clang++ -std=c++20 -c client.cpp -fmodule-file=math=other.pcm -o client.o
Damit komme ich zum GCC-Compiler.
Die mit C++20 eingeführten Concepts haben zusammen mit der Ranges-Bibliothek, Modulen und Coroutinen das Erstellen von modernen C++- Anwendungen neu definiert. Vom 7. bis zum 9. November 2023 bringt Sie Rainer Grimm in seinem Intensiv-Workshop C++20: die neuen Konzepte umfassend erklärt auf Stand und geht auf die vielen nützlichen Funktionen ein, die C++20 mitbringt.
GCC-Compiler
Die folgende Tabelle zeigt die wenigen GCC-Optionen:
Viele Optionen der Großen Drei addressieren Header Units.
Header Units
Header Units sind eine binäre Darstellung von Header-Dateien und stellen einen Übergang von Headern zu Modulen dar. Man muss die #include
-Direktive durch die neue Import
-Anweisung ersetzen und ein Semikolon (;
) hinzufügen.
#include <vector> => import <vector>;
#include "myHeader.h" => import "myHeader.h";
Weitere Informationen über Header Units finden sich in meinem vorherigen Artikel "Private Module Fragment and Header Units". In den folgenden Zeilen spiele ich mit Header Units und verwende die folgenden Dateien:
- Die Header-Datei
head.h
// head.h
#include <iostream>
void hello();
- Die Quelldatei
head.cpp
, die die Header Unit importiert
// head.cpp
import "head.h";
void hello() {
std::cout << '\n';
std::cout << "Hello World: header units\n";
std::cout << '\n';
}
- Das Hauptprogramm
helloWorld3.cpp
, das die Header Unit importiert
// helloWorld3.cpp
import "head.h";
int main() {
hello();
}
Ich werde eine Header Unit aus der Header-Datei head.h
für den Microsoft Visual Compiler und den GCC Compiler erstellen. Im Gegensatz zur offiziellen Dokumentation Standard C++ Modules konnte ich mit dem Clang Compiler keine Header Units erzeugen.
- Microsoft Visual Compiler
Dies sind die notwendigen Schritte, 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
/exportHeader
in Zeile 1 bewirkt, dass die ifc-Dateihead.h.ifc
aus der Header-Dateihead.h
erstellt wird. - Die Implementierungsdatei
head.cpp
(Zeile 2) und die Client-DateihelloWordl3.cpp
(Zeile 3) verwenden die Header-Unit. Das Flag/headerUnit head.h=head.h.ifc
importiert die Header-Unit und teilt dem Compiler den Namen der ifc-Datei für den angegebenen Header mit.
- GCC-Compiler
Die Erstellung und Verwendung des Moduls besteht aus drei Schritten.
g++ -fmodules-ts -fmodule-header head.h -std=c++20
g++ -fmodules-ts -c -std=c++20 head.cpp
g++ -fmodules-ts -std=c++20 head.o helloWorld3.cpp -o helloWorld3
- (1) erstellt das Modul
head.gcm
. Das Flag-fmodule-header
gibt an, dass aushead.h
eine Header Unit erstellt werden soll. - Die folgende Zeile erstellt die Objektdatei
head.o
. - Schließlich wird in Zeile 3 die ausführbare Datei erstellt, die implizit das Modul
head.gcm
verwendet.
Wie versprochen, folgt nun die kuriose Beobachtung.
Erreichbarkeit versus Sichtbarkeit
Bei Modulen muss man zwischen Erreichbarkeit und Sichtbarkeit unterscheiden. Wenn ein Modul eine Entität exportiert, kann ein importierender Client sie sehen und nutzen. Nicht exportierte Entitäten sind nicht sichtbar, können aber erreichbar sein.
// bar.cppm
module;
#include <iostream>
export module bar;
struct Foo {
void writeName() {
std::cout << "\nFoo\n";
}
};
export struct Bar {
Foo getFoo() {
return Foo{};
}
};
Das Modul bar
exportiert die Klasse Bar
. Bar
ist sichtbar und erreichbar. Foo
hingegen ist nicht sichtbar.
// bar.cpp
#include <utility>
import bar;
int main() {
Bar b;
// Foo f; // (1)
auto f = b.getFoo();
f.writeName(); // (2)
using FooAlias =
decltype(std::declval<Bar>().getFoo()); // (3)
FooAlias f2; // (4)
f2.writeName(); // (5)
}
Die Klasse Foo
wird nicht exportiert und ist daher nicht sichtbar. Ihre Verwendung in (1) würde einen Linker-Fehler verursachen. Im Gegenteil, Foo
ist erreichbar, weil die Mitgliedsfunktion getFoo
(in bar.cppm
) sie zurückgibt. Folglich kann die Funktion writeName
(2) aufgerufen werden. Außerdem kann ich einen Datentyp-Alias zu Foo
erstellen (3) und ihn verwenden, um Foo
zu instanziieren (4) und writeName
(5) aufzurufen. Der Ausdruck std::declval<Bar>().getFoo()
(3) gibt das Objekt zurück, das ein Aufruf Bar.getFoo()
zurückgeben würde. Schließlich gibt decltype
den Datentyp dieses hypothetischen Objekts zurück.
Wie geht's weiter?
In meinem nächsten Artikel werde ich tiefer in die Ranges-Bibliothek in C++20 eintauchen.
(rme)