C++20-Module: Private Module Fragment und Header Units

In den letzten Wochen habe ich etwas Neues zu Modulen in C++20 gelernt: Private Module Fragment und Header Units. Deshalb mache ich in diesem Beitrag einen kleinen Umweg über Module und stelle deren neue Funktionalität vor.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 8 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

In den letzten Wochen habe ich etwas Neues zu Modulen in C++20 gelernt: private Module Fragment und Header Units. Deshalb mache ich in diesem Beitrag einen kleinen Umweg über Module und stelle deren neue Funktionalität vor.

Du fragst dich vielleicht, warum ich meinen angekündigten Beitrag über Variadic Templates verschiebe? Der Grund ist einfach. Mein kommendes pdf-Bundle, das ich nächste Woche veröffentliche, beinhaltet die C++20-Module und ich möchte diesen Beitrag in das Bundle einbinden, daher ziehe ich dessen Veröffentlichung vor.

Ein private Module Fragment und Header Units machen den Umgang mit Modulen in C++20 deutlich komfortabler.

Ich verwende in diesem Beitrag absichtlich den neuesten Visual Studio Compiler, denn er unterstützt C++20-Module fast vollständig. Die neuesten GCC und Clang Compiler unterstützen Module hingegen nur teilweise.

Ich bin mir nicht sicher, ob du die Fakten über die Module Interface Unit und die Module Implementation Unit parat hast? Deshalb möchte ich die wichtigsten wiederholen.

Wenn du das Modul in ein Interface und dessen Implementierung separieren willst, bietet es sich an, es in eine Module Interface Unit und eine oder mehrere Module Implementation Units zu unterteilen.

// mathInterfaceUnit2.ixx

module;

#include <vector>

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

module math;

#include <numeric>

namespace math {

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 nicht-exportierende Moduldeklarationen: module math;.
  • Ein Modul kann mehr als eine Module Implementation Unit besitzen.

Das main-Programm

// client4.cpp

#include <iostream>
#include <vector>

import math;

int main() {

std::cout << '\n';

std::cout << "math::add(2000, 20): " << math::add(2000, 20) << '\n';

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

std::cout << "math::getProduct(myVec): " << math::getProduct(myVec) << '\n';

std::cout << '\n';

}

Aus der Sicht des Benutzers wurde nur der Namensraum math hinzugefügt.

Das manuelle Erstellen der ausführbaren Datei umfasst folgende Schritte.

cl.exe /std:c++latest /c mathInterfaceUnit2.ixx /EHsc                 // (1)
cl.exe /std:c++latest /c mathImplementationUnit2.cpp /EHsc // (2)
cl.exe /std:c++latest /c client4.cpp /EHsc // (3)
cl.exe client4.obj mathInterfaceUnit2.obj mathImplementationUnit2.obj // (4)
  1. Erzeugt die Objektdatei mathInterfaceUnit2.obj und die Modulschnittstellendatei math.ifc.
  2. Erzeugt die Objektdatei mathImplementationUnit2.obj.
  3. Erzeugt die Objektdatei client4.obj.
  4. Erzeugt die ausführbare Datei client4.exe.

Für den Microsoft-Compiler muss das Modell für die Ausnahmebehandlung (/EHsc) zusätzlich angeben werden. Verwende außerdem das Flag /std:c++latest.

Zum Abschluss liefert das Programm dann diese Ausgabe:

Einer der großen Vorteile der Strukturierung von Modulen in eine Module Interface Unit und eine oder mehrere Module Implementation Units ist, dass sich Änderungen in den Module Implementation Units nicht auf die Module Interface Unit auswirken und daher keine Neukompilierung erforderlich ist.

Dank eines private Module Fragment kannst du ein Modul in einer Datei implementieren und seinen letzten Teil mit module :private; als seine Implementierung deklarieren. Folglich führt eine Änderung des privaten Modulfragments nicht zu einer Neukompilierung des Moduls. Die folgende Moduldeklarationsdatei mathInterfaceUnit3.ixx fasst die Module Interface Unit mathInterfaceUnit2.ixx und die Module Implementation Unit mathImplementationUnit2.cpp in einer Datei zusammen.

// mathInterfaceUnit3.ixx

module;

#include <numeric>
#include <vector>

export module math;

export namespace math {

int add(int fir, int sec);

int getProduct(const std::vector<int>& vec);

}

module :private; // (1)

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>());
}

module: private; (Zeile 1) kennzeichnet den Beginn des private Module Fragments. Eine Änderung in diesem optionalen letzten Teil einer Moduldeklarationsdatei führt nicht dazu, dass sie neu kompiliert wird.

Ich habe die Header Units bereits in einem früheren Beitrag vorgestellt. Jetzt kann ich sie einsetzen.

Header Units sind ein einfacher Weg, um von Headern auf Module umzustellen. Du musst lediglich die #include-Direktive durch die neue import-Anweisung ersetzen.

#include <vector> => import <vector>;
#include "myHead.h" => import "myHead.h";

Erstens wendet import die gleichen Regeln wie include an, um Namen aufzulösen. Das bedeutet im Fall der Anführungszeichen ("myHeader.h"), dass das Lookup zuerst im lokalen Verzeichnis sucht, bevor es mit dem Systemsuchpfad fortfährt.

Zweitens ist dies weit mehr als eine Textersetzung. In diesem Fall erzeugt der Compiler aus der import-Anweisung etwas Modul-ähnliches und behandelt das Ergebnis so, als ob es ein Modul wäre. Die importierende Modulanweisung erhält alle exportierbaren Namen des Headers. Zu den exportierbaren Namen gehören auch Makros. Der Import dieser synthetischen Header-Einheiten ist schneller und in der Geschwindigkeit mit vorkompilierten Headern vergleichbar.

Vorkompilierte Header sind eine nicht standardisierte Methode, um Header in einer Zwischenform zu kompilieren, die vom Compiler schneller verarbeitet werden kann. Der Microsoft-Compiler verwendet die Erweiterung .pch und der GCC-Compiler .gch für vorkompilierte Header. Der Hauptunterschied zwischen vorkompilierten Headern und Modulen ist, dass Module selektiv Namen exportieren können. Nur in einem Modul exportierte Namen sind außerhalb des Moduls sichtbar.

Nach dieser kurzen Auffrischung möchte ich Header Units gerne einsetzen.

Das folgende Beispiel besteht aus drei Dateien. Die Header-Datei head.h, in der die Funktion hello deklariert wird, ihre Implementierungsdatei head.cpp und die Client-Datei helloWorld3.cpp, die die Funktion hello verwendet.

// head.h

#include <iostream>

void hello();

Nur die Implementierungsdatei head.cpp und die Client-Datei helloWorld3.cpp sind besonders. Sie importieren die Header-Datei head.h: import "head.h";.

// head.cpp

import "head.h";

void hello() {

std::cout << '\n';

std::cout << "Hello World: header units\n";

std::cout << '\n';

}
// helloWorld3.cpp

import "head.h";

int main() {

hello();

}

Die nachfolgenden Schritte sind notwendig, 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 /exporHeader (erste Zeile) bewirkt, dass die ifc-Datei head.h.ifc aus der Header-Datei head.h erstellt wird. Die ifc-Datei enthält die Metadatenbeschreibung der Modulschnittstelle.
  • Die Implementierungsdatei head.cpp (zweite Zeile) und die Client-Datei helloWordl3.cpp (dritte Zeile) verwenden die Header-Unit. Das Flag /headerUnit head.h=head.h.ifc importiert den Header und teilt dem Compiler oder Linker den Namen der ifc-Datei für den angegebenen Header mit.


Einen Nachteil haben Header Units aber doch: Nicht alle Header sind importierbar. Welche Header importierbar sind, hängt von der Implementierung ab. Der C++-Standard garantiert, dass alle Header der Standardbibliothek importierbar sind. Die Möglichkeit zu importieren, schließt C-Header aus.

In meinem nächsten Beitrag verwende ich Variadic Templates, um das C++-Idiom für eine vollständig generische Fabrik zu implementieren. Eine Implementierung dieses wichtigen C++-Idioms ist std::make_unique. ()