C++20: Ein einfaches math-Modul

Modernes C++  –  17 Kommentare

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 Erweiterung cpp. 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.ifc. 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.