Übersicht über Module in C++

C++20 führt Module ein. Ein Modul ist eine Reihe von Quellcodedateien, die unabhängig von den Quelldateien kompiliert werden (oder genauer gesagt, die Übersetzungseinheiten, die sie importieren.

Module beseitigen oder reduzieren viele der Probleme, die mit der Verwendung von Headerdateien verbunden sind. Sie verkürzen oftmals die Kompilierungszeiten, manchmal sogar erheblich. Makros, Präprozessordirektiven und nicht exportierte Namen, die in einem Modul deklariert sind, sind außerhalb des Moduls nicht sichtbar. Sie haben keine Auswirkungen auf die Kompilierung der Übersetzungseinheit, die das Modul importiert. Sie können Module in beliebiger Reihenfolge importieren, ohne sich um Makroneudefinitionen kümmern zu müssen. Deklarationen in der importierenden Übersetzungseinheit nehmen nicht an der Überladungsauflösung oder Namenssuche (Namens-Lookup) im importierten Modul teil. Nachdem ein Modul einmal kompiliert wurde, werden die Ergebnisse in einer Binärdatei gespeichert, die alle exportierten Typen, Funktionen und Vorlagen beschreibt. Der Compiler kann diese Datei viel schneller verarbeiten als eine Headerdatei. Und der Compiler kann ihn an jedem Ort wiederverwenden, an dem das Modul in ein Projekt importiert wird.

Sie können Module nebeneinander mit Headerdateien verwenden. Eine C++-Quelldatei kann import-Module und auch #include-Headerdateien enthalten. In einigen Fällen können Sie eine Headerdatei als Modul importieren, was schneller ist, als sie mit dem Präprozessor mithilfe von #include zu verarbeiten. Es wird empfohlen, Module in neuen Projekten anstelle von Headerdateien so weit wie möglich zu verwenden. Bei größeren bestehenden Projekten, die sich in aktiver Entwicklung befinden, sollten Sie mit der Umwandlung von Legacy-Headern in Module experimentieren. Richten Sie Ihre Entscheidung danach, ob sich die Kompilierungszeiten deutlich verkürzen.

Informationen zum Kontrast von Modulen mit anderen Methoden zum Importieren der Standardbibliothek finden Sie unter Vergleichen von Headereinheiten, Modulen und vorkompilierten Headern.

Aktivieren von Modulen im Microsoft C++-Compiler

Ab Visual Studio 2022, Version 17.1, werden C++20-Standardmodule vollständig im Microsoft C++-Compiler implementiert.

Vor der Angabe durch den C++20-Standard hatte Microsoft experimentelle Unterstützung für Module. Der Compiler unterstützt auch das Importieren vordefinierter Standardbibliotheksmodule, die unten beschrieben werden.

Ab Visual Studio 2022, Version 17.5, wird das Importieren der Standardbibliothek als Modul sowohl standardisiert als auch vollständig im Microsoft C++-Compiler implementiert. In diesem Abschnitt wird die ältere experimentelle Methode beschrieben, die noch unterstützt wird. Informationen zur neuen standardisierten Methode zum Importieren der Standardbibliothek mithilfe von Modulen finden Sie unter Importieren der C++-Standardbibliothek mithilfe von Modulen.

Sie können die Modulfunktion verwenden, um Module für einzelne Partitionen zu erstellen und die von Microsoft bereitgestellten Module der Standardbibliothek zu importieren. Um Support für Standardbibliotheksmodule zu aktivieren, kompilieren Sie mit /experimental:module und /std:c++latest. Klicken Sie in einem Visual Studio-Projekt mit der rechten Maustaste auf den Projektknoten im Projektmappen-Explorer und wählen Sie Eigenschaften aus. Legen Sie die Dropdownliste Konfiguration auf Alle Konfigurationen fest, und wählen Sie dann Konfigurationseigenschaften>C/C++>Sprahe>C++-Module aktivieren (experimentell).

Ein Modul und der Code, der es verwendet, müssen mit denselben Compileroptionen kompiliert werden.

Verwenden der C++-Standardbibliothek als Module (experimentell)

In diesem Abschnitt wird die experimentelle Implementierung beschrieben, die weiterhin unterstützt wird. Die neue standardisierte Methode der Verwendung der C++-Standardbibliothek als Module wird unter Importieren der C++-Standardbibliothek mithilfe von Modulen beschrieben.

Indem Sie die C++-Standardbibliothek als Module importieren, anstatt sie über Headerdateien einzugeben, können Sie die Kompilierungszeiten je nach Größe des Projekts möglicherweise beschleunigen. Die experimentelle Bibliothek ist in die folgenden benannten Module unterteilt:

  • std.regex stellt den Inhalt des Headers bereit <regex>
  • std.filesystem stellt den Inhalt des Headers bereit <filesystem>
  • std.memory stellt den Inhalt des Headers bereit <memory>
  • std.threading stellt den Inhalt der Header <atomic>, <condition_variable>, <future><mutex>, <shared_mutex> und <thread> bereit
  • std.core stellt alles andere in der C++-Standardbibliothek bereit

Um diese Module zu nutzen, fügen Sie oben in der Quellcodedatei eine Importdeklaration hinzu. Beispiel:

import std.core;
import std.regex;

Um die Microsoft-Standardbibliotheksmodule zu nutzen, kompilieren Sie Ihr Programm mit den Optionen /EHsc und /MD.

Beispiel

Das folgende Beispiel zeigt eine einfache Moduldefinition in einer Quelldatei namens Example.ixx. Die .ixx-Erweiterung ist für Modulschnittstellendateien in Visual Studio erforderlich. In diesem Beispiel enthält die Schnittstellendatei sowohl die Funktionsdefinition als auch die Deklaration. Sie können die Definitionen jedoch auch in einer oder mehreren separaten Modulimplementierungsdateien platzieren, wie in einem späteren Beispiel gezeigt.

Die export module Example;-Anweisung gibt an, dass diese Datei die primäre Schnittstelle für ein Modul namens Example ist. Der export Modifizierer f() gibt an, dass diese Funktion sichtbar ist, wenn ein anderes Programm oder Modul Example importiert.

// Example.ixx
export module Example;

#define ANSWER 42

namespace Example_NS
{
   int f_internal() {
        return ANSWER;
      }

   export int f() {
      return f_internal();
   }
}

Die Datei MyProgram.cpp verwendet import für den Zugriff auf den Namen, der von Example exportiert wird. Die Namespacebezeichnung Example_NS ist hier sichtbar, aber nicht alle Member, weil sie nicht exportiert werden. Außerdem ist das Makro ANSWER nicht sichtbar, da Makros nicht exportiert werden.

// MyProgram.cpp
import Example;
import std.core;

using namespace std;

int main()
{
   cout << "The result of f() is " << Example_NS::f() << endl; // 42
   // int i = Example_NS::f_internal(); // C2039
   // int j = ANSWER; //C2065
}

Die import-Deklaration kann nur auf globaler Ebene angezeigt werden.

Modulgrammatik

module-name:
module-name-qualifier-seqoptidentifier

module-name-qualifier-seq:
identifier .
module-name-qualifier-seq identifier .

module-partition:
: module-name

module-declaration:
exportoptmodulemodule-namemodule-partitionoptattribute-specifier-seqopt;

module-import-declaration:
exportoptimportmodule-nameattribute-specifier-seqopt;
exportoptimportmodule-partitionattribute-specifier-seqopt;
exportoptimportheader-nameattribute-specifier-seqopt;

Implementieren von Modulen

Eine Modulschnittstelle exportiert den Modulnamen und alle Namespaces, Typen, Funktionen usw., welche die öffentliche Schnittstelle des Moduls bilden.
Eine Modulimplementierung definiert die vom Modul exportierten Elemente.
In seiner einfachsten Form kann ein Modul eine einzelne Datei sein, welche die Modulschnittstelle und -implementierung kombiniert. Sie können die Implementierung auch in eine oder mehrere separate Modulimplementierungsdateien einfügen, ähnlich wie .h- und .cpp-Dateien dies tun.

Bei größeren Modulen können Sie Teile des Moduls in Untermodule unterteilen, die Partitionen genannt werden. Jede Partition besteht aus einer Modulschnittstellendatei, die den Modulpartitionsnamen exportiert. Eine Partition kann auch über eine oder mehrere Partitionsimplementierungsdateien verfügen. Das Modul insgesamt verfügt über eine primäre Modulschnittstelle, welche die öffentliche Schnittstelle des Moduls ist. Sie kann die Partitionsschnittstellen bei Bedarf exportieren.

Ein Modul besteht aus einer oder mehreren Moduleinheiten. Eine Moduleinheit ist eine Übersetzungseinheit (eine Quelldatei), die eine Moduldeklaration enthält. Es gibt mehrere Typen von Moduleinheiten:

  • Eine Modulschnittstelleneinheit exportiert einen Modulnamen oder einen Modulpartitionsnamen. Eine Modulschnittstelleneinheit weist export module in der Moduldeklaration auf.
  • Eine Modulimplementierungseinheit exportiert keinen Modulnamen oder Modulpartitionsnamen. Wie der Name schon sagt, implementiert es ein Modul.
  • Eine primäre Modulschnittstelleneinheit exportiert den Modulnamen. Es muss eine und nur eine primäre Modulschnittstelleneinheit in einem Modul vorhanden sein.
  • Eine Modulpartitionseinheit exportiert einen Modulpartitionsnamen.
  • Eine Modulpartitionsimplementierungseinheit hat einen Modulpartitionsnamen in der Moduldeklaration, aber kein export-Schlüsselwort.

Das export-Schlüsselwort wird nur in Schnittstellendateien verwendet. Eine Implementierungsdatei kann import ein anderes Modul, aber keine export Namen haben. Implementierungsdateien können eine beliebige Erweiterung haben.

Module, Namespaces und argumentabhängiges Lookup

Die Regeln für Namespaces in Modulen sind dieselben wie für jeden anderen Code. Wenn eine Deklaration innerhalb eines Namespace exportiert wird, wird der eingeschlossene Namespace (ausgenommen Elemente, die nicht explizit in diesen Namespace exportiert werden) ebenfalls implizit exportiert. Wenn ein Namespace explizit exportiert wird, werden alle Deklarationen innerhalb dieser Namespacedefinition exportiert.

Wenn der Compiler argumentabhängiges Lookup nach Überladungsauflösungen in der importierenden Übersetzungseinheit durchführt, berücksichtigt er Funktionen, die in derselben Übersetzungseinheit (einschließlich Modulschnittstellen) deklariert sind, in welcher der Typ der Funktionsargumente definiert ist.

Modulpartitionen

Eine Modulpartition ähnelt einem Modul, außer:

  • Sie teilt die Eigentümerschaft an allen Deklarationen im gesamten Modul.
  • Alle von Partitionsschnittstellendateien exportierten Namen werden von der primären Schnittstellendatei importiert und exportiert.
  • Der Name einer Partition muss mit dem Modulnamen beginnen, gefolgt von einem Doppelpunkt (:).
  • Deklarationen in einer der Partitionen sind innerhalb des gesamten Moduls sichtbar.\
  • Es sind keine besonderen Vorsichtsmaßnahmen erforderlich, um ODR-Fehler (One Definition-Rule) zu vermeiden. Sie können einen Namen (Funktion, Klasse usw.) in einer Partition deklarieren und in einer anderen definieren.

Eine Partitionsimplementierungsdatei beginnt wie folgt und ist eine interne Partition aus C++-Standards:

module Example:part1;

Eine Partitionsschnittstellendatei beginnt wie folgt:

export module Example:part1;

Um auf Deklarationen in einer anderen Partition zuzugreifen, muss eine Partition sie importieren. Sie kann jedoch nur den Partitionsnamen und nicht den Modulnamen verwenden:

module Example:part2;
import :part1;

Die primäre Schnittstelleneinheit muss alle Schnittstellenpartitionsdateien des Moduls wie folgt importieren und erneut exportieren:

export import :part1;
export import :part2;

Die primäre Schnittstelleneinheit kann Partitionsimplementierungsdateien importieren, aber nicht exportieren. Diese Dateien dürfen keine Namen exportieren. Mit dieser Einschränkung kann ein Modul implementierungsinterne Details für das Modul beibehalten.

Module und Headerdateien

Sie können Headerdateien in eine Modulquelldatei einfügen, indem Sie eine #include-Anweisung vor der Moduldeklaration einfügen. Diese Dateien werden als im globalen Modulfragment befindlich betrachtet. Ein Modul kann nur die Namen im globalen Modulfragment sehen, die sich in den Headern befinden, die es explizit enthält. Das globale Modulfragment enthält nur Symbole, die verwendet werden.

// MyModuleA.cpp

#include "customlib.h"
#include "anotherlib.h"

import std.core;
import MyModuleB;

//... rest of file

Sie können eine herkömmliche Headerdatei verwenden, um zu steuern, welche Module importiert werden:

// MyProgram.h
import std.core;
#ifdef DEBUG_LOGGING
import std.filesystem;
#endif

Importierte Headerdateien

Einige Header sind so eigenständig, dass sie mit dem import-Schlüsselwort eingebracht werden können. Der Hauptunterschied zwischen einem importierten Header und einem importierten Modul besteht darin, dass alle Präprozessordefinitionen im Header unmittelbar nach der import-Anweisung im Importprogramm sichtbar sind.

import <vector>;
import "myheader.h";

Siehe auch

module, import, export
Tutorial zu benannten Modulen
Vergleichen von Headereinheiten, Modulen und vorkompilierten Headern