C++20: Modulunterstützung der großen drei Compiler

Module gehören zu den vier wichtigen Neuerungen in C++20. Zeit für einen Blick darauf, wie gut die Großen Drei (GCC, Clang und MSVC) mit Modulen arbeiten.

In Pocket speichern vorlesen Druckansicht 28 Kommentare lesen

(Bild: Shutterstock.com/Kenishirotie)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Module sind eine der großen vier Neuerungen in C++20. In meinen C++20-Klassen sind sie eines der Hauptthemen. Leider lag die Implementierung in GCC und Clang weit hinter der des Microsoft Compilers zurück. Deshalb habe ich in meinen Klassen, Vorträgen und Büchern meist den Microsoft Compiler verwendet, um Module vorzustellen. Ich freue mich, dass sich die Modulunterstützung von GCC und Clang verbessert hat. Deshalb werde ich in diesem Artikel den aktuellen Stand der Modulimplementierung (10/2023) der großen drei Compiler GCC, Clang und MSVC vorstellen.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

In den letzten vier Jahren habe ich fast 100 Artikel über C++20 geschrieben, aber ich bin noch nicht fertig. In meinem nächsten Artikel werde ich meine Geschichte über C++20 fortsetzen.

Wer mit Modulen in C++20 nicht vertraut ist, findet hier ein einfaches Beispiel:

Dies ist das Modul math.

// math1.ixx

module;                  // (1)          

#include <numeric>
#include <vector>

export module math;      // (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>());
}

Das globale Modulfragment beginnt mit dem Schlüsselwort module (1) und endet mit der exportierenden Moduldeklaration (3). Das globale Modulfragment ist der Ort, an dem Präprozessoranweisungen wie #include verwendet werden, damit das Modul kompiliert werden kann. Präprozessor-Entities, die innerhalb des globalen Modulfragments verwendet werden, sind nur innerhalb des Moduls sichtbar. Das Modul math exportiert die beiden Funktionen add und getProduct.

Der Client importiert das Modul math (1) und nutzt seine Funktionen:

// client1.cpp

#include <iostream>
#include <vector>

import math;                        // (1)

int main() {
    
    std::cout << '\n';   
   
    std::cout << "add(2000, 20): " << add(2000, 20) << '\n';
    
    std::vector<int> myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    std::cout << "getProduct(myVec): " << getProduct(myVec) << '\n';
    
    std::cout << '\n';
   
}

Dies ist die Ausgabe des Programms:

Mehr über Module findet sich in folgenden Artikeln:

  1. Die Vorteile von Modulen
  2. Ein einfaches math Modul
  3. Module Interface Unit und Module Implementation Unit
  4. Module strukturieren
  5. Weitere offene Fragen zu Modulen
  6. Private Module Fragment und Header Units

Ich habe mit einem einfachen Modul begonnen und werde es noch einfacher machen, wenn ich den Modulimplementierungsstatus von GCC, Clang und MSVC vorstelle.

iX-Workshop mit Rainer Grimm: C++20 Deep Dive

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.

math.ixx definiert das Modul math, das ich im folgenden Vergleich verwenden werde.

// math.ixx

export module math;     // (1)

export int add(int fir, int sec){
    return fir + sec;
}

Außerdem ist hier das Client-Programm client.cpp, das das Modul math importiert.

// client.cpp

import math;       // (2)

int main() {
   
   add(2000, 20);
   
}

(1) ist die exportierende Moduldeklaration, und (2) importiert das Modul. Aus offensichtlichen Gründen werde ich die Ausgabe des Programms ignorieren.

Warum ich die Moduldeklarationsdatei math.ixx genannt habe, könnte zunächst irritieren.

  • Der Microsoft-Compiler verwendet das Suffix ixx. Das Suffix ixx steht für eine Modul-Interface Datei.
  • Der Clang-Compiler verwendet das Suffix cppm. Das m im Suffix steht wohl für Modul.
  • Der GCC-Compiler verwendet keine spezielle Erweiterung.

Dies sind nur die Konventionen, die sich ändern lassen. Nun werde ich das Modul math kompilieren und verwenden.

Zuerst beginne ich mit Microsofts cl.exe 19.29.30133 für x64 Compiler.

  • Microsoft-Compiler

Dies sind die Schritte zum Kompilieren und Verwenden des Moduls mit dem Microsoft-Compiler. Ich zeige nur die minimale Befehlszeile. Wie versprochen, werde ich im nächsten Artikel mehr dazu schreiben. Bei einem älteren Microsoft-Compiler muss statt dem Flag /std:c++20 das Flag /std:c++latest verwendet werden.

cl.exe /std:c++20 /c math.ixx
cl.exe /std:c++20 client.cpp math.obj
  • Zeile 1 erstellt eine Objekt-Datei math.obj und eine IFC-Datei math.ifc. Die IFC-Datei ist das Modul und enthält die Metadatenbeschreibung der Modulschnittstelle. Das Binärformat der IFC-Datei folgt dem Format Internal Program Representation von Gabriel Dos Reis und Bjarne Stroustrup.
  • Zeile 2 erstellt die ausführbare Datei client.exe. Der Compiler findet implizit die kompilierte math.ifc Datei aus dem ersten Schritt.

Weiter geht es mit dem Clang-Compiler.

  • Clang-Compiler

Ich verwende den Clang 16.0.5 Compiler.

Der Clang-Compiler erwartet ein Modul mit der Erweiterung cppm. Daher werde ich die Datei math.ixx in math.cppm umbenennen. Die Datei client.cpp lässt sich hingegen unverändert weiterverwenden. Abschließend sind hier die entsprechenden Schritte zum Erstellen und Verwenden des Moduls:

clang++ -std=c++20 -c math.cppm --precompile -o math.pcm
clang++ -std=c++20 client.cpp -fprebuilt-module-path=. math.pcm -o client.exe
  • Zeile 1 erstellt das Modul math.pcm. Die Endung pcm steht für precompiled module und entspricht der ifc-Dateierweiterung des Microsoft Visual Compilers. Außerdem enthält das erzeugte Modul bereits die Moduldefinition. Folglich erzeugt der Clang-Compiler keine Objektdatei math.o. Die Option --precompile ist notwendig, um das vorkompilierte Modul zu erstellen.
  • Zeile 2 erstellt die ausführbare Datei client.exe, die das Modul math.pcm verwendet. Der Clang-Compiler benötigt den Pfad zu dem Modul: -fprebuilt-module-path. Falls dieser nicht bekannt ist, schlägt der Link-Prozess fehl:

Zum Abschluss verwende ich den GCC-Compiler.

  • GCC-Compiler

Nun kommt der GCC 11.1.0 Compiler zum Einsatz.

Der GCC-Compiler erwartet weder das Suffix ixx von Windows noch das Suffix cppm von Clang. Deshalb benenne ich die Datei math.ixx in eine cpp-Datei um: math.cxx. Die Datei client.cpp ist identisch mit der, die ich mit dem Microsoft und dem Clang-Compiler verwendet habe.

g++ -c -std=c++20 -fmodules-ts math.cxx
g++ -std=c++20 -fmodules-ts client.cpp math.o -o client
  • Zeile 1 erstellt das Modul math.gcm und die Objektdatei math.o. Ich muss dafür das Flag -fmodules-ts angeben. Die Erweiterung -fmodules-ts irritiert mich, weil ts normalerweise für technische Spezifikation steht. Das Modul math.gcm liegt im Unterverzeichnis gcm.cache. math.gcm ist die kompilierte Modulschnittstelle. Das Suffix gcm steht wohl für GCC compiled module.
  • Zeile 2 erstellt den ausführbaren Client. Er verwendet implizit das Modul math.gcm.

In diesem Artikel habe ich dir die ersten Schritte gezeigt, wie du ein Modul mit den Big Three erstellen kannst. In meinem nächsten Artikel werde ich tiefer in die Modulunterstützung der Großen Drei einsteigen. (rme)