Zum Inhalt springen

C++20-Module

Lerne, warum Module Header ablösen, wie du Module deklarierst und exportierst, was Modulpartitionen sind und wie der Übergang von header-basiertem Code gelingt.

Warum Module die Header ablösen

Das #include-System ist seit Jahrzehnten die Grundlage der C++-Kompilierung, bringt aber ernste Probleme mit sich. Header werden textuell eingebunden — der Präprozessor kopiert die gesamte Datei in jede Übersetzungseinheit. Das heißt: #include fügt womöglich über 50.000 Zeilen Code in jede .cpp-Datei ein. Die Folge: langsame Kompilierung, fragile Include-Reihenfolge (Makros aus einem Header beeinflussen einen anderen) und Verletzungen der One Definition Rule (ODR), die stilles undefiniertes Verhalten auslösen.

C++20-Module lösen all diese Probleme. Ein Modul wird einmal zu einer binären Schnittstelle kompiliert und danach von Konsumenten importiert, ohne erneut geparst zu werden. Es gibt keine textuelle Einbindung, kein Durchsickern von Makros und keine Abhängigkeit von der Include-Reihenfolge. Bei großen Projekten können sich die Build-Zeiten um das 5- bis 20-Fache verbessern.

Moduldeklaration und Export

Ein Modul hat eine Interface-Unit (die deklariert, was exportiert wird) und optional eine oder mehrere Implementation-Units. Das Schlüsselwort export macht Deklarationen für Importeure sichtbar:

math_utils.cppm
// math_utils.cppm — module interface unit
export module math_utils;    // module declaration

import <cmath>;               // import a header unit

// Exported — visible to importers
export double circle_area(double radius) {
    return std::numbers::pi * radius * radius;
}

export struct Point {
    double x, y;

    double distance_to(const Point& other) const {
        double dx = x - other.x;
        double dy = y - other.y;
        return std::sqrt(dx * dx + dy * dy);
    }
};

// NOT exported — module-private (internal linkage)
double helper_function(double val) {
    return val * val;
}

// Export a namespace — all members become visible
export namespace geometry {
    double triangle_area(double base, double height) {
        return 0.5 * base * height;
    }

    double rectangle_area(double width, double height) {
        return width * height;
    }
}

Module importieren

Konsumenten greifen mit import auf die exportierten Deklarationen eines Moduls zu. Anders als #include bindet import nichts textuell ein — Makros und Implementierungsdetails sickern nicht über Modulgrenzen hinweg durch:

main.cpp
// main.cpp — imports the module
import math_utils;

#include <iostream>  // can still use #include for non-modularized headers

int main() {
    double area = circle_area(5.0);
    std::cout << "Circle area: " << area << "\n";

    Point p1{0.0, 0.0};
    Point p2{3.0, 4.0};
    std::cout << "Distance: " << p1.distance_to(p2) << "\n";  // 5.0

    std::cout << "Triangle: " << geometry::triangle_area(10, 5) << "\n";

    // helper_function(42);  // ERROR: not exported, not visible

    return 0;
}

// Compile order matters for modules:
// 1. Compile the module interface first:
//    g++ -std=c++20 -fmodules-ts -c math_utils.cppm
// 2. Then compile the consumer:
//    g++ -std=c++20 -fmodules-ts main.cpp -o main

Modulpartitionen

Große Module lassen sich in Partitionen aufteilen — Teilmodule, die zusammen die vollständige Modulschnittstelle bilden. Partitionen sind modulintern und können von externen Konsumenten nicht direkt importiert werden:

module_partitions.cppm
// --- math_utils-algebra.cppm (partition interface) ---
export module math_utils:algebra;

export double square(double x) { return x * x; }
export double cube(double x) { return x * x * x; }

// --- math_utils-trig.cppm (partition interface) ---
export module math_utils:trig;

import <cmath>;

export double deg_to_rad(double degrees) {
    return degrees * std::numbers::pi / 180.0;
}

export double sin_deg(double degrees) {
    return std::sin(deg_to_rad(degrees));
}

// --- math_utils.cppm (primary module interface) ---
export module math_utils;

// Re-export partitions — consumers see everything
export import :algebra;
export import :trig;

// Additional exports can go here
export double hypotenuse(double a, double b) {
    return std::sqrt(square(a) + square(b));
}

// --- Consumer ---
// import math_utils;  // gets algebra, trig, and hypotenuse

import std (C++23)

C++23 führt import std; ein, das die gesamte Standardbibliothek als Modul importiert. Für die Kompiliergeschwindigkeit ist das ein Wendepunkt:

import std;  // Alles: iostream, vector, string, algorithm, ...

int main() {
    std::println("Hello from C++23 modules!");
    std::vector<int> v{1, 2, 3};
    std::ranges::sort(v);
}

import std; ersetzt Dutzende von #include-Direktiven durch einen einzigen Import, der dramatisch schneller kompiliert, weil das Standardbibliotheksmodul vorkompiliert ist. Es gibt außerdem import std.compat;, das zusätzlich die Namen der C-Bibliothek im globalen Namensraum bereitstellt (etwa printf, strlen).

Stolperfalle

Die Compiler-Unterstützung reift noch: Stand 2024 unterstützen GCC, Clang und MSVC Module, aber in unterschiedlicher Vollständigkeit und mit unterschiedlicher Build-System-Integration. MSVC hat die ausgereifteste Unterstützung.

Build-System-Integration: CMake ab 3.28 unterstützt Module über das Feature CXX_MODULE_SETS, aber das Ökosystem (Dependency-Scanner, Paketmanager) zieht erst nach. Die Build-Reihenfolge spielt nun eine Rolle — Modulschnittstellen müssen vor ihren Konsumenten kompiliert werden.

Kein Makro-Export: Module exportieren #define-Makros bewusst nicht. Code, der auf Makros aus Headern angewiesen ist (wie WIN32_LEAN_AND_MEAN oder NOMINMAX), muss das anders lösen.

Migrationsstrategie: Beginne mit import std; (C++23), um die Includes der Standardbibliothek zu ersetzen, und modularisiere dann schrittweise deinen eigenen Code, angefangen bei Blattbibliotheken mit wenigen Abhängigkeiten.

Best Practice

Wenn du Module einführst:

1. Halte Modulschnittstellen schlank — exportiere nur, was Konsumenten brauchen. Nicht exportierte Entitäten haben Modul-Linkage und können keine ODR-Verletzungen verursachen.
2. Nutze Partitionen für große Module — teile sie in logische Teilmodule auf (z.B. :types, :algorithms, :io).
3. Bevorzuge export namespace — exportiere ganze Namensräume statt einzelner Deklarationen, damit die Schnittstelle übersichtlich bleibt.
4. Mische #include und import nicht für dieselbe Bibliothek — entscheide dich für eines und bleibe innerhalb einer Übersetzungseinheit konsistent.
5. Verwende die Endungen .cppm oder .ixx für Moduldateien mit Schnittstellen, damit Build-Systeme sie automatisch erkennen.

Wichtigste Erkenntnisse
  • Module ersetzen #include durch import — keine textuelle Einbindung, kein Makro-Durchsickern, keine Fehler durch Include-Reihenfolge
  • Mit export machst du Deklarationen für Importeure sichtbar; nicht exportierte Entitäten bleiben modulprivat
  • Modulpartitionen zerlegen große Module in Teilkomponenten und präsentieren dennoch eine einheitliche Schnittstelle
  • import std; (C++23) ersetzt alle Includes der Standardbibliothek durch einen einzigen, schnellen Import
  • Build-Systeme müssen Modulschnittstellen vor den Konsumenten kompilieren — die Kompilierreihenfolge zählt

Quiz — Teste dein Wissen

(15 XP)

1. Was ist der Hauptvorteil von Modulen gegenüber #include?

2. Was passiert mit einer Funktion, die in einem Modul definiert, aber NICHT mit `export` markiert ist?

3. Was bewirkt `import std;` in C++23?