C++20: Weitere offene Fragen zu Modulen

Modernes C++  –  2 Kommentare

Die bisherigen vier Artikel zu Modulen gingen auf deren Grundlagen ein. Daher gibt es nur noch wenige Fragen zu Modulen zu beantworten. Genau diese Fragen adressiere ich in dem heutigen Artikel: Templates und Module, das Linkage von Modulen und Header Units.

In diesem Artikel mache ich es mir einfach. Ich nehme an, dass du meine vorherigen Artikel zu Modulen kennst. Falls nicht, habe ich sie hier aufgelistet:

Templates und Module

Ich höre recht häufig die Frage: Wie werden Templates durch Module exportiert? Wenn du ein Template instanziierst, muss seine Definition verfügbar sein. Das ist der Grund, warum Template-Definitionen in Header-Dateien verpackt werden. Konzeptionell besitzt die Verwendung von Templates die folgende Struktur.

  • templateSum.h
// templateSum.h

template <typename T, typename T2>
auto sum(T fir, T2 sec) {
return fir + sec;
}
  • sumMain.cpp
// sumMain.cpp

#include <templateSum.h>

int main() {

sum(1, 1.5);

}

Die Datei sumMain.cpp inkludiert direkt die Header-Datei templatSum.h. Der Aufruf sum(1, 1.5) stößt die Template-Instanziierung an. In diesem Fall erzeugt der Compiler aus dem Funktions-Template sum die konkrete Funktion sum, die ein int und ein double erwartet. Dieser Prozess lässt sich schön mit C++ Insights visualisieren.

Templates können und sollten mit C++20 in Modulen definiert werden. Module habe eine eindeutige interne Repräsentation, die weder Sourcecode noch Assembler entspricht. Diese Repräsentation ist eine Art Abstract Syntax Tree (AST). Dank ihr steht die Template-Definition während der Template-Instanziierung zur Verfügung.

Im folgenden Beispiel definiere ich das Funktions-Template sum in dem Modul math.

  • mathModuleTemplate.ixx
// mathModuleTemplate.ixx

export module math;

export namespace math {

template <typename T, typename T2>
auto sum(T fir, T2 sec) {
return fir + sec;
}

}
  • clientTemplate.cpp
// clientTemplate.cpp

#include <iostream>
import math;

int main() {

std::cout << std::endl;

std::cout << "math::sum(2000, 11): " << math::sum(2000, 11) << std::endl;

std::cout << "math::sum(2013.5, 0.5): " << math::sum(2013.5, 0.5) << std::endl;

std::cout << "math::sum(2017, false): " << math::sum(2017, false) << std::endl;

}

Die Kommendozeile zum Übersetzen des Programms unterscheidet sich nicht von der des letzten Artikels "C++20: Module strukturieren". Daher verzichte ich auf diese und zeige direkt die Ausgabe des Programms.

Mit Modulen erhalten wir eine neue Art von Linkage.

Modul Linkage

Bisher besitzt C++ zwei Arten von Linkage: Internal Linkage und External Linkage.

  • Internal Linkage: Namen mit Interal Linkage lassen sich nicht außerhalb der Übersetzungseinheit verwenden. Es schließt im Wesentlichen globale Namen (namespace scope) ein, die als static deklariert sind, und Mitglieder anonymer Namensräume.
  • External Linkage: Namen mit External Linkage lassen sich außerhalb der Übersetzungseinheit verwenden. Es schließt im Wesentlichen globale Namen ein, die nicht static deklariert, aber auch Klassen und ihre Mitglieder, Variablen und Templates.

Mit Modulen erhalten wir Module Linkage.

  • Module Linkage: Namen mit Module Linkage lassen sich nur innerhalb des Moduls verwenden. Damit Namen Module Linkage besitzen, sind zwei Bedingungen notwendig. Einerseits dürfen die Namen keine Internal Linkage haben und damit nur in der Übersetzungseinheit sichtbar sein und andererseits dürfen die Namen nicht exportiert werden.

Die kleine Variation des vorherigen Moduls math soll Module Linkage verdeutlichen. Ich möchte Anwendern meines Funktions-Templates sum zusätzlich zurückgeben, welchen Rückgabetyp der Compiler deduziert hat.

  • mathModuleTemplate1.ixx
// mathModuleTemplate1.ixx

module;

#include <iostream>
#include <typeinfo>
#include <utility>

export module math;

template <typename T> // (2)
auto showType(T&& t) {
return typeid(std::forward<T>(t)).name();
}

export namespace math { // (3)

template <typename T, typename T2>
auto sum(T fir, T2 sec) {
auto res = fir + sec;
return std::make_pair(res, showType(res)); // (1)
}

}

Anstelle der Summe der Zahlen gibt das Funktions-Template sum ein std::pair (1) zurück, das aus der Summe und der String-Repräsentation des Datentyps res besteht. Ich habe das Funktions-Template showType (2) nicht im exportierten Namensraum math (3) verwendet. Konsequenterweise kann auf showType nicht außerhalb des Moduls math zugegriffen werden. showType nutzt Perfect Forwarding, um die Wertkategorie des Funktionsarguments t zu erhalten. Die Funktion typeid ermittelt die Information zum Datentyp zur Laufzeit (runtime type identification (RTTI)).

  • clientTemplate1.cpp
// clientTemplate1.cpp

#include <iostream>
import math;

int main() {

std::cout << std::endl;

auto [val, message] = math::sum(2000, 11);
std::cout << "math::sum(2000, 11): " << val << "; type: " << message << std::endl;

auto [val1, message1] = math::sum(2013.5, 0.5);
std::cout << "math::sum(2013.5, 0.5): " << val1 << "; type: " << message1 << std::endl;

auto [val2, message2] = math::sum(2017, false);
std::cout << "math::sum(2017, false): " << val2 << "; type: " << message2 << std::endl;

}

Jetzt stellt das Programm das Ergebnis der Summation und die String-Repräsentation des automatisch ermittelten Rückgabetyps dar.

Weder der GCC noch der Clang Compiler unterstützen das nächste Feature. Es wird wohl sehr gerne eingesetzt werden.

Header Units

Header Units stellen eine sehr angenehme Art dar, den Übergang von Header-Dateien und Modulen zu vollziehen. Du musst dazu lediglich die #include-Direktive durch die neue import-Direktive ersetzen.

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

Zuerst einmal respektiert import dieselben Lookup-Regeln wie include. Das bedeutet in dem konkreten Fall der Anführungszeichen ("myHeaders.h"), dass der Lookup zuerst das lokale Verzeichnis und dann den Systempfad berücksichtigt.

Darüber hinaus stellt die Ersetzung von #include mit import mehr als eine Textersetzung dar. Im Fall von import erzeugt der Compiler eine Einheit, die modulähnlich ist, und behandelt diese wie ein Modul. Der import-Aufruf importiert alle Namen der Header-Datei, die exportierbar sind. Dies umfasst alle Makros. Das Importieren der erzeugten Header Unit ist schneller und aus Performanzsicht vergleichbar zum Importieren vorkompilierter Header-Dateien.

Ein Wermutstropfen

Header Units besitzen einen Wermutstropfen: Nicht alle Header-Dateien sind importierbar. Welche Header-Dateien das sind, hängt vom Compiler ab. Der C++20-Standard garantiert aber, dass alle Header-Dateien des Standards importierbar sind. Das schließt die C-Header-Dateien aus, die nur durch den Namensraum std dekoriert werden. So ist zum Beispiel die Header-Datei <cstring> der C++-Wrapper für <string.h>. Du kannst die C-Header-Dateien einfach identifizieren, denn aus C-Header-Datei xxx.h wird cxxx.

Wie geht's weiter

Mit diesem Artikel schließe ich meine Geschichte zu Modulen und insbesondere zu den großen Vier in C++20 ab. Der Link C++20 verweist auf alle existierenden und noch erscheinenden Artikel zu C++20. Als Nächstes nehme ich die Features der Kernsprache genauer unter die Lupe, die nicht so prominent sind wie Concepts oder Module. Der nächste Artikel beschäftigt sich mit dem Drei-Weg-Vergleichsoperator.