C++ Tutorial

 Module

Bisherige Vorgehensweise

Ist eine Anwendung umfangreicher, wird sie in logische Blöcke unterteilt, z.B. ein Block für die Eingaben, einen für Berechnungen und einen für die Ausgaben. Jeder dieser Blöcke besteht aus einem Implementierungsteil, der Quellcode-Datei .cpp, und dem Schnittstellenteil, der Header-Datei .h. Der Schnittstellenteil enthält nur die Deklarationen der zu veröffentlichen globalen Funktionen sowie die extern-Deklarationen der zu veröffentlichen Daten.

Das nachfolgende Beispiel zeigt anhand der kleinen Bibliothek bibl die Aufteilung des Codes in den Schnittstellenteil und Implementierungsteil.

// Datei bibl.h
// Schnittstellen der Bibliothek bibl.h
// Zu veroeffentliche Daten und Funktionen
extern const char *const pText;
float CircleArea(float rad);

// Datei bibl.cpp
// Implementierung der Bibliothek bibl.cpp
#include "bibl.h"

// Globales const-Datum initialisieren
const char *const pText = "bibl";
// Lokale Konstante
constexpr float PI = 3.1416f;

// Lokale Funktion
float Quad(float val)
{
   return val * val;
}
// Globale Funktion
float CircleArea(float rad)
{
   return PI * Quad(rad);
}

// Datei main.cpp
// Anwendung
#include <iostream>
#include <format>
#include "bibl.h" // Bibliotheks-Schnittstellen einbinden

int main()
{
   // Bibliotheksname ausgeben
   std::cout << std::format("Bibliothek: {}\n",pText);
   // Kreisflaeche berechnen
   float radius = 2.2f;
   std::cout << std::format("Kreisradius: {}, ergibt Kreisflaeche: {}\n", radius, CircleArea(radius));
}

Einer der Nachteile der Aufteilung des Codes in einen Schnittstellenteil und Implementierungsteil ist, dass bei Änderungen an den Schnittstellen zwei Dateien anzupassen sind.

Des Weiteren werden die Schnittstellendateien beim Einbinden in eine Quellcode-Datei jedes Mal neu vom Compiler übersetzt und dies kann bei umfangreichen Schnittstellendateien erhebliche Zeit beanspruchen. Um u.a. diese Nachteile zu umgehen, wurden mit C++20 die sogenannten Module eingeführt.

Modul-Definition

Ein Modul kann sowohl den Schnittstellenteil wie auch Implementierungsteil in einer Datei enthalten. Eine Modul-Datei sieht zunächst aus wie eine 'normale' Quellcode-Datei. Erst durch die die Anweisung

export module ModName;

wird daraus eine Modul-Datei, wobei ModName der frei wählbare Name des Moduls ist. Vor dieser Anweisung dürfen nur Kommentare und die Definition des globalen Moduls (wird gleich noch erläutert) stehen.

Damit der Compiler weiß, welche Daten und Funktionen aus der Modul-Datei zu veröffentlichen sind, wird vor dem Datentyp des Datums bzw. vor dem Returntyp der Funktion das Schlüsselwort export gestellt.

Die kleine Bibliothek als Modul würde damit wie folgt aussehen:

// Implementierung der Bibliothek bibl.cxx als Modul
export module Bibl;

// Zu veroeffentliches const-Datum initialisieren
export const char *pText = "bibl";
// Nicht zu veroeffentliche Konstante
constexpr float PI = 3.1416f;

// Lokale Funktion
float Quad(float val)
{
   return val * val;
}
// Zu veroeffentliche Funktion
export float CircleArea(float rad)
{
   return PI * Quad(rad);
}

Modul-Dateien müssen vor den sonstigen Quellcode-Dateien vom Compiler übersetzt werden, damit er die zu exportierenden Namen extrahieren kann. Je nach Compiler erfolgt dies bis zum jetzigen Zeitpunkt unterschiedlich:

Visual Studio

Im Projektmappen-Explorer die Eigenschaften der Modul-Datei öffnen. Anschließend auf die Eigenschaft C/C++ – Erweitert klicken und auf der rechten Seite unter Kompilierungsart die Option Als C++-Modulcode kompilieren auswählen.

CodeBlocks mit MinGW

Hier sind keine weiteren Einstellungen notwendig, da diese bereits bei der Konfiguration des Compilers unter CodeBlocks entsprechend vorgenommen wurden. Sie müssen nur explizit im Workspace durch Auswahl der Option Build file die Modul-Datei vor der Quellcode-Datei übersetzen.

Einbinden Modul-Datei

Um auf die exportierten Daten und Funktionen aus einer Modul-Datei zuzugreifen, ist das Modul mit der Anweisung

import ModName;

zu importieren.

// Anwendung
#include <iostream>
#include <format>

// Modul einbinden
import Bibl;

int main()
{
   // Bibliotheksname ausgeben
   std::cout << std::format("Bibliothek: {}\n",pText);
   // Kreisflaeche berechnen
   float radius = 2.2f;
   std::cout << std::format("Kreisradius: {}, ergibt Kreisflaeche: {}\n", radius, CircleArea(radius));
}

Globales Modul

Stehen in einer Modul-Datei #include-Anweisungen, müssen diese im globalen Modul eingebunden werden. Das globale Modul wird durch die Anweisung

module;

definiert und muss vor der export module Anweisung stehen. Im globalen Modul dürfen nur #include-Anweisungen und Kommentare stehen.

// Globales Modul mit include-Anweisungen
module;
#include <cmath>

// Implementierung der Bibliothek bibl.cxx als Modul
export module Bibl;

// Lokale Konstanten
const float GConst = 9.81f;                  // G-Konstante
const float DEG2RAD = 2.f * 3.1416f / 360.f; // Umrechnung Deg->Rad

// Wurfweite = (Geschwindigkeit)^2 * sin(2*Winkel) / G-Konstante
export float Distance(float velocity, float angel)
{
   float res = velocity * velocity * std::sin(2.0f*angel*DEG2RAD) / GConst; return res;
}

// Bibliotheksdatei bibl.cxx
// =========================
// Globales Modul fuer includes
module;
#include <iostream>
#include <format>

// Modul exportieren
export module Checksum;

// Berechnung der Pruefsumme exportieren
// pData: Zeiger auf Datenfeld
// len : Groesse des Datenfelds in Bytes
export unsigned char CalcCS(const unsigned char* pData, size_t len)
{
   // Pruefsumme initialisieren
   unsigned char checksum = pData[0];
   // Alle Bytes des Datenfeldes exklusiv verodern
   for (decltype(len) index = 1; index < len; index++)
      checksum ^= pData[index];
   // Pruefsumme zurueckgeben
   return checksum;
}

// Ueberpruefen des Inhalt des Feldes auf Fehler
export bool CheckCS(const unsigned char* pData, size_t len, unsigned int cs)
{
   // Pruefsumme berechnen
   unsigned char checksum = pData[0];
   for (decltype(len) index = 1; index < len; index++)
      checksum ^= pData[index];
   // Pruefsumme mit Vorgabe vergleichen und im Fehlerfall
   // eine Meldung ausgeben
   bool CSOk = (cs == checksum);
   if (!CSOk)
      std::cout << std::format("Pruefsummenfehler! Soll:{}, Ist:{}\n", cs, checksum);
   // Ergebnis der Ueberpruefung zurueckgeben
   return CSOk;
}

// Anwendung Datei main.cpp
// ========================
#include <iostream>

// Pruefsummen-Modul importieren
import Checksum;

int main()
{
   // Zu pruefende int-Daten
   int data[]{ 10,20,30,40,50,60,70,80,90 };
   // Pruefsumme berechnen
   auto checksum = CalcCS(reinterpret_cast<unsigned char*>(data), sizeof(data));
   // Pruefsumme testen
   auto res = CheckCS(reinterpret_cast<unsigned char*>(data), sizeof(data), checksum);
   if (res)
      std::cout << "Alles ok!\n";
   // Ein int-Datum modizieren
   data[2] = 99;
   // Nochmals Pruefsumme testen
   res = CheckCS(reinterpret_cast<unsigned char*>(data), sizeof(data), checksum);
   if (res)
      std::cout << "Alles ok!\n";
}