C++20: Ein einfaches math-Modul
Module sind eines der prominenten vier Features von C++20. Im heutigen Artikel geht es um die Erzeugung des einfachen Moduls math.
- Rainer Grimm
Module sind eines der prominenten vier Features von C++20. Sie überwinden die Einschränkungen von Header-Dateien und versprechen noch mehr: schnellere Kompilierungszeiten, weniger Verletzungen der One Definition Rule und den selteneren Einsatz von Makros. Im heutigen Artikel erzeuge ich ein einfaches Modul math
.
Die lange Geschichte der Module in C++
2004 schrieb Daveed Vandevoorde das Proposal N1736. In ihm brachte er das erste Mal die Idee von Modulen. Es dauert aber bis 2012, bis der C++-Standard eine eigene Study Group (SG2, Modules) zu Modulen erhielt. 2017 boten Clang 5.0 und MSVC 19.1 erste Implementierungen von Modulen an. Ein Jahr später war das Module TS (Technical Specification) fertig. Parallel dazu schlug Google das sogenannte ATOM-Proposal (Another Take on Modules) vor: PO947. 2019 wurden beide Proposals im Entwurf zum C++20-Standard zusammengeführt: N4842. Auf diesem Entwurf basieren meine Artikel zu Modulen.
Der C++-Standardisierungsprozess ist durch und durch demokratisch. Der Abschnitt Standardization gibt mehr Details zum Standard und seinem Standardisierungsprozess. Das Bild stellt die verschiedenen Study Groups vor.
Module aus der Sicht von Anwendern vorzustellen ist einfach. Diese Beobachtung gilt – wie fast immer in C++ – nicht für die Sicht der Implementierer. Mein Plan für diesen und die folgenden Artikel ist es, mit einem einfachen Modul math
zu starten und sukzessive neue Features zu dem Modul hinzuzufügen.
Das math-Modul
Hier ist mein erstes Modul:
// math.ixx
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 lässt sich damit von den Anwendern verwenden:
// client.cpp
import math;
int main() {
add(2000, 20);
}
import math
importiert das Modul math
und bewirkt, dass die exportierten Namen des Moduls in der Datei client.cpp
sichtbar werden. Das war der einfache Teil meiner Aufgabe. Die Herausforderung beginnt, wenn ich das Programm übersetze.
Die Modul-Deklarationsdatei
Ist dir der eigentümliche Name math.ixx
des Moduls aufgefallen?
- cl.exe (Microsoft) verwendet das Suffix
ixx
. Es steht für Modul Interface Source. - Clang verwendet das Suffix cppm. Es steht wohl für Modul-Deklaration. Falsch!!! Die Dokumentation zu Clang führt in die falsche Richtung. Verwende nicht die Erweiterung
cppm
, bis du meinen nächsten Artikel gelesen hast. Nutze einfach die Erweiterungcpp
. Du wirst nicht die gleiche Odyssee wie ich durchleben wollen. - Ich kenne kein Suffix für den GCC.
Das Modul math kompilieren
Um das Modul zu übersetzen, benötigst du einen aktuellen Clang-, GCC- oder cl.exe-Compiler. Ich werde in diesem Artikel den cl.exe-Compiler von Windows einsetzen. Der Microsoft-Blog besitzt zwei exzellente Einführungsartikel zu Modulen: "Overview of modules in C++" und "C++ Modules conformance improvements with MSVC in Visual Studio 2019 16.5". Im Gegensatz dazu stellen die fehlenden Einführungen zu Clang und GCC eine hohe Hürde für den Einsatz von Modulen dar.
Hier sind mehr Details zu meinem Microsoft-Compiler:
Dieses sind die Schritte, um das Modul mit dem Microsoft-Compiler zu übersetzen und zu verwenden. Ich stelle nur die minimale Kommandozeile dar. Mit einem älteren Microsoft-Compiler muss man zumindest noch das Flag /std:cpplatest
verwenden.
cl.exe /experimental:module /c math.ixx // 1
cl.exe /experimental:module client.cpp math.obj // 2
Zuerst erzeuge eine Objektdatei math.obj
und eine IFC-Datei math.if
c. Die IFC-Datei enthält die Metadaten zur Beschreibung des Modul-Interfaces. Das binäre Format der IFC-Datei folgt der Internal Program Representation, die Gabriel Dos Reis und Bjarne Stroustrup bereits 2004/2005 definiert haben.
Dann erzeuge die ausführbare Datei client.exe
. Ohne die implizit verwendete math.ifc-
Datei des ersten Schritts findet der Linker das Modul nicht.
Aus verständlichen Gründen zeige ich nicht die Ausführung des Programms. Das ändert sich aber sofort.
Globales Modul-Fragment
Dank des Global Modul Fragment lassen sich Module einfach zusammenstellen. Damit kann man #include
-Direktiven direkt im Modul verwenden. Der Code im Global Modul Fragment wird durch das Modul nicht exportiert.
Meine zweite Version des Moduls math
bietet die Funktionen add
und getProduct
an:
// math1.ixx
module; // global module fragment (1)
#include <numeric>
#include <vector>
export module math; // module declartion (2)
export int add(int fir, int sec){
return fir + sec;
}
export int getProduct(const std::vector<int>& vec) {
return std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>());
}
Ich habe die notwendigen Header-Dateien zwischen dem Global Modul Fragment (Zeile 1) und der Modul-Deklaration (Zeile 2) inkludiert:
// client1.cpp
#include <iostream>
#include <vector>
import math;
int main() {
std::cout << std::endl;
std::cout << "add(2000, 20): " << add(2000, 20) << std::endl;
std::vector<int> myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << "getProduct(myVec): " << getProduct(myVec) << std::endl;
std::cout << std::endl;
}
Das Programm importiert das Modul math
und verwendet seine Funktionalität:
Ich nehme an, du willst die Header-Dateien der Standard Template Library nicht mehr verwenden. Microsoft bietet bereits Module für alle STL-Header-Dateien an. Hier sind die Details aus dem Blogartikel "Using C++ Modules in Visual Studio 2017" des Microsoft-C++-Team-Blogs:
std.regex
bietet den Inhalt der Header-Datei <regex
> an.std.filesystem
offeriert den Inhalt der Header-Datei <experimental/filesystem
> an.std.memory
zeigt den Inhalt der Head-Datei <memory
> an.std.threading
bietet den Inhalt der Header-Dateien <atomic
>, <condition_variable
>, <future
>, <mutex
>, <shared_mutex
> und <thread
> an.std.core
umfasst den Rest der C++-Standard-Bibliothek.
Um die Microsoft Standard Library Module zu verwenden, musst du Exception Handling (/EHsc
) und die Multithreading-Bibliothek (/MD
) einsetzen. Darüber hinaus benötigst du noch das Flag /std:c++latest
.
Hier sind die leicht modifizierten Dateien der Interface-Datei math2.ixx
und der Source-Datei client2.cpp
.
math2.ixx
// math2.ixx
module;
import std.core; // (1)
export module math;
export int add(int fir, int sec){
return fir + sec;
}
export int getProduct(const std::vector<int>& vec) {
return std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>());
}
client2.cpp
// client2.cpp
import std.core; // (1)
import math;
int main() {
std::cout << std::endl;
std::cout << "add(2000, 20): " << add(2000, 20) << std::endl;
std::vector<int> myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << "getProduct(myVec): " << getProduct(myVec) << std::endl;
std::cout << std::endl;
}
Beide Dateien verwenden in der Zeile (1) das Modul std.core.
Wie geht's weiter?
Meine ersten Module math.ixx
, math1.ixx
und math2.ixx
definieren ihre Funktionalität in einer Datei. In meinem nächsten Artikel werde ich die Definition der Module in eine sogenannte Modul Interface Unit und eine Modul Implementation Unit aufteilen.
()