C++20: Module Interface Unit und Module Implementation Unit

Modernes C++  –  14 Kommentare

Dank Module Interface Unit und Module Implementation Unit lässt sich die Definition eines Moduls in sein Interface und seine Implementierung aufteilen. Der heutige Artikel zeigt, wie sich das umsetzen lässt.

Wie ich in meinem letzten Artikel "C++20: Ein einfaches math-Modul" angekündigt habe, werde ich diesen Artikel mit meiner Clang-Odyssee beginnen. Mein kleiner Umweg ist zugleich eine kompakte Wiederholung meines letzten Artikels.

Meine Clang-Odyssee

Aufgrund der Vorträge von Boris Kolpackov "Building C++ Modules" auf der CppCon 2017 oder Corentin Jabot "Modules are not a tooling opportunity" hatte ich geglaubt, dass die Compiler-Hersteller die folgenden Suffixe für Module vorschlagen:

  • Windows: ixx
  • Clang: cppm
  • GCC: kein Suffix

Im Fall des Clang-Compilers lag ich daneben. Das ist das einfache math-Modul, das ich mit dem Clang-Compiler übersetzen wollte:

// math.cppm

export module math;

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

Ich verwendete zum Übersetzen den Clang-9- und den Clang-10-Compiler auf Microsoft und Linux. Darüber hinaus baute ich den aktuellen Clang 11 direkt aus den Quellen und setzte ihn ein. Alle meine Versuche, das Modul zu erzeugen, endeten mit einer ähnlichen Fehlermeldung:

Die Kommandozeile sollte das Modul math.pcm erzeugen. Ich verwendete die Kommandozeile -std=c++20 -fmodules-ts, aber die Fehlermeldung ergab: module interface compilation requires '-std=c++20' or '-fmodules-ts'. Ich spielte alle Variationen der beiden Flags durch, fügte das globale Modul-Fragment der Modul-Definition hinzu und versuchte es noch mit weiteren Flags. Das Ergebnis war immer dasselbe.

Dann bat ich Arthur O'Dwyer und Roland Bock um ihre Hilfe. Arthur hatte bereits Module mit dem Clang erzeugt: "Hello World with C++2a modules". Roland hatte wie ich den Clang 11 direkt aus den Quellen gebaut und mein math-Modul direkt erzeugen können. Wir beiden hatten buchstäblich denselben Clang-Compiler und dieselbe Moduldefinition verwendet. Buchstabe für Buchstabe verglich ich seine Kommandozeile mit meiner. Da fiel mir der entscheidende Unterschied auf:

Meine:   clang++ -std=c++20 - -fmodules-ts -stdlib=libc++ -c math.cppm -Xclang -emit-module-interface -o math.pcm
Roland: clang++ -std=c++20 - -fmodules-ts -stdlib=libc++ -c math.cpp -Xclang -emit-module-interface -o math.pcm

Roland nannte seine Moduldefinition math.cpp. cpp war genau das Suffix, das auch Arthur eingesetzt hatte. Gib deinem Modul nicht das Suffix cppm.

Jetzt waren das Erzeugen und das Verwenden des Moduls ein Kinderspiel:

Um diesen Exkurs zu beenden, stelle ich kurz noch die client.cpp-Datei vor und sage noch ein paar Worte zu den Flags für den Clang-Compiler:

// client.cpp

import math;

int main() {

add(2000, 20);

}
clang++ -std=c++2a -stdlib=libc++ -c math.cpp -Xclang -emit-module-interface -o math.pcm // (1)
clang++ -std=c++2a -stdlib=libc++ -fprebuilt-module-path=. client.cpp math.pcm -o client // (2)
  1. Erzeugt das Modul math.pcm. Das Suffix pcm steht für ein vorkompiliertes Modul (precompiled modul). Die Kombination der Flags -Xclang -emit-module-interface ist für die Erzeugung des vorkompilierten Moduls notwendig.
  2. Erzeugt die ausführbare Datei client, die das Modul math.pcm verwendet. Dazu musst du den Pfad zu dem Modul mit dem Flag -fprebuilt-module-path angeben.

Das Modul math war sehr einfach gestrickt. Das nächste Modul soll ein wenig anspruchsvoller werden.

Regel für die Struktur eines Moduls

Hier ist die erste Regel für die Struktur eines Moduls:

module;                      // global module fragment

#include <headers for libraries not modularized so far>

export module math; // module declartion

import <importing of other modules>

<non-exported declarations> // names with only visibiliy inside the module

export namespace math {

<exported declarations> // exported names

}

Diese Regel hilft in doppelter Hinsicht. Sie gibt dir eine einfache Struktur für ein Modul vor und eine Idee, worüber ich noch schreiben werde. Was ist daher neu in der Struktur des Moduls?

  • In Module lassen sich andere Module importieren. Sie haben Modulbindung und sind nicht außerhalb des Moduls sichtbar. Diese Beobachtung gilt auch für die nichtexportierten Deklarationen (non-exported declarations).
  • Ich habe die exportierten Namen in einen Namensraum math verpackt. Er besitzt denselben Namen wie das Modul.
  • Die Namen in dem Modul sind lediglich deklariert und nicht definiert. Nun möchte ich auf die Trennung des Interfaces und der Implementierung eines Moduls eingehen.

Module Interface Unit und Module Implementation Unit

Entsprechend der Regel zum Aufbau eines Moduls möchte ich das Modul math des letzten Artikels "C++20: Ein einfaches math-Modul" refaktorieren.

Module Interface Unit
// mathInterfaceUnit.ixx

module;

import std.core;

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 exportierte Modul-Deklaration: export module math.
  • Die Namen add und getProduct werden exportiert.
  • Ein Modul kann nur eine Module Interface Unit besitzen.
Module Implementation Unit
// mathImplementationUnit.cpp

module math;

import std.core;

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 die nicht exportierte Moduldeklaration: module math.
  • Ein Modul kann mehrere Module Implementation Units besitzen.
Das main-Programm
// client3.cpp

import std.core;

import math;

int main() {

std::cout << std::endl;

std::cout << "math::add(2000, 20): " << math::add(2000, 20) << std::endl;

std::vector<int> myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

std::cout << "math::getProduct(myVec): " << math::getProduct(myVec) << std::endl;

std::cout << std::endl;

}
  • Für Anwender ändert sich nicht viel. Sie müssen lediglich den Namensraum math verwenden.
Bauen der ausführbaren Datei

Das händische Bauen der ausführbaren Datei besteht aus ein paar Schritten:

cl.exe /std:c++latest /c /experimental:module mathInterface.ixx /EHsc /MD          // (1)
cl.exe /std:c++latest /c /experimental:module mathImplementationUnit.cpp /EHsc /MD // (2)
cl.exe /std:c++latest /c /experimental:module client3.cpp /EHsc /MD // (3)
cl.exe client3.obj mathInterfaceUnit.obj mathImplementationUnit.obj // (4)
  1. Erzeugt die Objektdatei mathInterfaceUnit.obj und die Module-Interface-Datei math.ifc.
  2. Erzeugt die Objektdatei mathImplementationUnit.obj.
  3. Erzeugt die Objektdatei client3.obj.
  4. Erzeugt die Objektdatei client3.exe.

Für den Microsoft-Compiler müssen das Exception Handling (/EHsc) und die Multithreading-Bibliothek (/MD) verwendet werden. Darüber hinaus ist das Flag /std:c++latest notwendig.

Zu guter Letzt ist hier die Ausgabe des Programms:

Wie geht's weiter?

In meinem nächsten Artikel werde ich das Modul math um weitere Features erweitern. Zuerst werde ich Module in ein Modul importieren und die importierten Module als Bestandteil eines neuen Moduls anbieten; darüber hinaus wird das nächste Modul Namen enthalten, die nur innerhalb des Moduls sichtbar sind.