Module
Sollte bei der Verwendung von CodeBlocks und dem MinGW-Compiler beim Übersetzen eines Projekts, welches Module enthält, die Fehlermeldung "No such file or directory..." angezeigt werden, übersetzen Sie die Anwendung einfach nochmals. Der Grund für die Fehlermeldung ist, dass unter CodeBlocks keine Reihenfolge der zu übersetzenden Dateien spezifiziert werden kann. Und Modul-Dateien müssen immer vor den sonstigen Dateien zu übersetzt werden,
Ist eine Anwendung umfangreicher, wird der Quellcode in der Regel auf mehrere Dateien aufgeteilt. Die Aufteilung der Anwendung erfolgt dabei sinnvollerweise so, dass logisch zusammengehörende Funktionen in einem 'Block' zusammengefasst werden. Und jeder dieser 'Blöcke' besteht aus einer Quellcode-Datei mit den Definitionen und einer Header-Datei mit den Deklarationen der nach außen hin sichtbaren Daten und Funktionen.
// Datei math1.h
// Deklarationen der Mathematik-Funktionen
namespace math1
{
double sin(double val);
double cos(double val);
}
// Datei math1.cpp
// Definitionen der Mathematik-Funktionen
#include "math1.h"
#include <print>
// In einer realen Anwendung hier den Sinus
// per Reihenentwicklung berechen
double math1::sin(double val)
{
std::println("Hier Sinus({}) berechnen.",val);
return 0.7; // Dummy Returnwert
}
// In einer realen Anwendung hier den Cosinus
// per Reihenentwicklung berechen
double math1::cos(double val)
{
std::println("Hier Cosinus({}) berechnen.",val);
return 0.3; // Dummy Returnwert
}
Benötigt eine Quellcode-Datei Daten oder Funktionen aus einem 'Block', bindet sie mittels #include "..." die entsprechende Header-Datei ein. Eine Anwendung der Funktionen sin() und cos() könnte wie folgt aussehen:
// Datei main.cpp
#include <print>
#include "math1.h" // Einbinden der Mathe-Funktionen
int main()
{
// Cosinus ausgeben
auto result = math1::sin(2.0);
std::println("Sinus berechnet: {}",result);
std::println("Cosinus berechnet: {}",math1::cos(2.0));
}
Hier Sinus(2) berechnen.
Sinus berechnet: 0.7
Hier Cosinus(2) berechnen.
Cosinus berechnet: 0.3
Eines der Probleme bei diesem Vorgehen ist, dass z.B. bei einer Änderung der Funktionssignatur von sin() oder cos() Anpassungen in den beiden Dateien math1.h und math1.cpp vorgenommen müssen. Um dies zu umgehen, können Module eingesetzt werden. Sie bieten u.a. folgende Vorteile gegenüber der bisherigen Vorgehensweise:
- Die Aufteilung in Header-Datei und Quellcode-Datei kann entfallen, da in der Quellcode-Datei eines Moduls explizit festlegt wird, welche Daten und Funktionen nach außen hin sichtbar sind.
- Eine in Module aufgeteilte Anwendung wird schneller übersetzt, da ein Modul nur einmal übersetzt wird und dabei die veröffentlichten Symbolen intern in einer Tabelle ablegt werden. Beim Einbinden von Header-Dateien mittels #include dagegen wird in jeder Quellcode-Datei jede eingebundene Header-Datei neu übersetzt.
- Es gibt (fast) keine Abhängigkeit, in welcher Reihenfolge Module in einer Quellcode-Datei einzubinden sind.
Modul-Definition
Um eine Quellcode-Datei als Modul-Datei zu definieren, wird die export-Anweisung
export module MNAME;
eingefügt, wobei MNAME den Namen des Moduls festlegt. Der Modulname darf optional einen Punkt enthalten, der aber keine syntaktische Bedeutung hat und nur dazu dient, Modulnamen lesbarer zu gestalten.
Vor der export-Anweisung dürfen nur Leerzeilen, Kommentare oder die Definition eines globalen Moduls (wird gleich erklärt) stehen. Daraus folgt auch, dass in einer Quellcode-Datei nur ein Modul definiert werden kann.
Um auf die Daten und Funktionen in einem Modul von außerhalb des Moduls zugreifen zu können, wird vor dem Datentyp des Datums bzw. vor dem Returntyp der Funktion ebenfalls das Schlüsselwort export gestellt.
Unsere kleine Mathe-Bibliothek könnte damit wie folgt aussehen:
// Datei math1.cxx
module; // Das wird gleich noch
#include <print> // erklaert!
// Modul math1 exportieren
export module math1;
// Der 'Inhalt' eines Moduls in einen
// Namensraum einschliessen
namespace math1
{
// In einer realen Anwendung hier den Sinus
// per Reihenentwicklung berechen
export double sin(double val)
{
std::println("Hier Sinus({}) berechnen.",val);
return 0.7; // Dummy Returnwert
}
// In einer realen Anwendung hier den Cosinus
// per Reihenentwicklung berechen
export double cos(double val)
{
std::println("Hier Cosinus({}) berechnen.",val);
return 0.3; // Dummy Returnwert
}
}
Bitte beachten Sie, dass ein Modul kein Namensraum ist. So führen gleichnamige Daten und Funktionen in unterschiedlichen Modulen zu einem Namenskonflikt. Deshalb mein Tipp: Definieren Sie zumindest die zu exportierenden Daten und Funktionen in einem Namensraum, so wie im Beispiel angegeben. Sinnvollerweise geben Sie dem Namensraum den gleichen Namen wie dem Modul.
Für Modul-Dateien gibt es keine allgemein gültige Extension. In den Lösungen zu den Übungen erhalten Modul-Dateien die Extension .cxx und die später erwähnten Modul-Schnittstellen-Dateien die Extension .ixx.
Damit Visual Studio eine Datei als Modul-Datei übersetzt, ist bei den Datei-Eigenschaften unter C/C++ – Erweitert – Kompilierungsart die Option Als C++-Modulcode kompilieren auszuwählen.
Import eines Moduls
Um auf die von einem Modul exportierten Daten und Funktionen zuzugreifen, ist das Modul zu importieren.
import MNAME;
wobei MNAME wieder der Name des zu importierenden Moduls ist.
// Datei main.cpp
#include <print>
// math1 Modul einbinden
import math1;
int main()
{
// Cosinus ausgeben
auto result = math1::sin(2.0);
std::println("Sinus berechnet: {}",result);
std::println("Cosinus berechnet: {}",math1::cos(2));
}
Hier Sinus(2) berechnen.
Sinus berechnet: 0.7
Hier Cosinus(2) berechnen.
Cosinus berechnet: 0.3
Die Angabe von math1 beim Aufruf der Funktion sin() bezieht sich auf den Namensraum in dem die Funktion definiert ist und nicht auf das Modul!
Das Einbinden eines Moduls ist nicht nur auf 'normale' Quellcode-Dateien beschränkt, sondern ein Modul kann ebenfalls ein weiteres Modul importieren.
// Modul-Datei any.cxx
export module any; // Export des Moduls
import some; // Modul some wird nicht exportiert!
export import other; // Modul other Import und Export
In Zeile 3 wird das Modul some importiert. Die Daten und Funktionen aus diesem Modul sind nur innerhalb des Moduls any verfügbar. Das Modul other hingegen wird nach dem Import wieder exportiert, sodass dessen Daten und Funktionen ebenfalls beim importieren des Moduls any zur Verfügung stehen.
#include in Modulen (globales Modul)
Da die export-Anweisung die erste Anweisung in einem Modul sein muss, dürfen vor dieser Anweisung keine Header-Dateien mittels #include eingebunden werden. Was aber tun, wenn in einer Modul-Datei eine Header-Datei benötigt wird? Die Lösung ist die Definition eines globalen Moduls innerhalb des Moduls.
Das globale Modul wird durch die Anweisung
module;
definiert und muss vor der export-Anweisung stehen. Nach der Definition des globalen Moduls können die benötigten Header-Dateien eingebunden werden.
// Datei math1.cxx
// Das globale Modul mit den includes
module;
#include <print>
// Modul math1 exportieren
export module math1;
// Der 'Inhalt' eines Moduls in einen
// Namensraum einschliessen
namespace math1
{
// Definitionen/Deklarationen des Moduls
}
Wie im Kapitel Ausgabestream cout erwähnt, könnten auch die Standardbiliotheksfunktion mittels
import std; // für die C++-Funktionen
import std.compat; // für die C-Funktionen
importiert werden und damit die vielen #include-Anweisungen am Programmanfang entfallen. Da jedoch zum Zeitpunkt der Erstellung des Tutorials noch kein Compiler dies vollständig unterstützt, werden weiterhin #include-Anweisungen verwendet.
Aufteilung in Modulschnittstelle und -implementierung
Bei umfangreichen Modulen kann es sinnvoll sein, die Implementierung von dessen Schnittstelle (zu exportierende Daten und Funktionen) zu trennen. Damit ist es möglich, bei einer Änderung in der Implementierung nur das Modul neu zu übersetzen und nicht die gesamte Anwendung, da sich die Schnittstelle nach außen hin nicht geändert hat.
Um die Schnittstelle von der Implementierung zu trennen, wird das Modul in zwei Dateien aufgeteilt.
Die Datei mit der Modul-Implementierung enthält als Erstes die Anweisung module MNAME; (also ohne export). Alle Funktionen des Moduls werden wie 'normale' Funktionen definiert, ohne Berücksichtigung, ob sie exportiert werden oder nicht.
// Datei math1.cxx
// Enthaelt die Implementierung des Moduls
module; // Globales Modul
#include <print> // fuer #include
// Modul math1
module math1;
// Namensraum fuer Mathe-Biblothek
namespace math1
{
// In einer realen Anwendung hier den Sinus
// per Reihenentwicklung berechen
double sin(double val)
{
std::println("Hier Sinus({}) berechnen.",val);
return 0.7; // Dummy Returnwert
}
// In einer realen Anwendung hier den Cosinus
// per Reihenentwicklung berechen
double cos(double val)
{
std::println("Hier Cosinus({}) berechnen.",val);
return 0.3; // Dummy Returnwert
}
}
Erst in der Datei mit der Modul-Schnittstelle werden das Modul und die zu veröffentlichen Funktionen exportiert.
// Datei math1a.ixx
// Datei mit Schnittstelle des Moduls
// Fuer den GCC ist ein abweichender
// Dateiname erforderlich
// Modul exportierten
export module math1;
// und Funktionen exportieren
namespace math1
{
export double sin(double val);
export double cos(double val);
}
In der Anwendung ändert sich nichts, d.h das Modul wird weiterhin mittels import math1; importiert.
Bei Verwendung von Visual Studio ist nur bei der Datei mit der Modul-Schnittstelle die Option Als C++-Modulcode kompilieren zu setzen. Die Datei mit der Modul-Implementierung ist als 'normale' C++-Datei zu übersetzen.
Modul-Partitionen
Wird die Implementierung eines Moduls noch umfangreicher, kann es auf mehrere Dateien aufgeteilt werden, d.h., das Modul wird partitioniert. Ein partitioniertes Modul besteht immer aus einer übergeordneten Modul-Datei sowie den Dateien mit den Modul-Partitionen.
Die übergeordnete Modul-Datei exportiert in der ersten Anweisung das Modul. Anschließend werden die einzelnen Modul-Partitionen mit der Anweisung
[export] import :PNAME
importiert, wobei PNAME der Name der zu importierenden Modul-Partition ist. Soll der Inhalt der importierten Modul-Partition nach außen hin sichtbar sein, ist sie explizit zu exportieren.s
// Uebergeordnete Moduldatei greet.cxx
export module greet; // Modul exportieren
export import : en; // Import Partition en exportieren
export import : de; // Import Partition de exportieren
In der Partitionsdatei wird die Partition mit der Anweisung
export module MNAME:PNAME
exportiert. Die Anweisung exportiert nur den Namen der Modul-Partition. Alle zu exportierenden Daten und Funktionen müssen zusätzlich gekennzeichnet werden.
// Datei greet_de.cxx mit der Partition de
// Modulpartition exportieren
export module greet:de;
// Definition/Deklarationen exportieren
export namespace language
{
const char* German(void)
{
return "Willkommen!";
}
}
// Datei greet_en.cxx mit der Partition en
// Modulpartition exportieren
export module greet:en;
// Definition/Deklarationen exportieren
export namespace language
{
const char* English(void)
{
return "Welcome!";
}
}
Beachten Sie, dass im Beispiel der Namensraum language exportiert wird. Wird ein Namensraum exportiert, werden alle in ihm definierten Daten und Funktion exportiert.
Eine mögliche Anwendung könnte dann wie folgt aussehen:
// Datei main.cpp
#include <print>
import greet;
int main()
{
std::println("en: {}", language::English());
std::println("de: {}", language::German());
}
en: Welcome!
de: Willkommen!
Übungen
modul_01:
Diese Übung ist eine Abwandlung der Übung funk_01 zu Funktionen. Die Funktionen zum Berechnen und Prüfen der Prüfsumme sind nun in einem Modul cs zu definieren.
Schreiben Sie eine Funktion, die für ein zu übergebendes unsigned char-Feld eine unsigned char-Prüfsumme berechnet und diese zurückliefert. Die Prüfsumme ist durch Exklusiv-Veroderung aller Feldelemente zu berechnen.
Schreiben Sie eine zweite Funktion, die als Parameter das Feld sowie die dazugehörige Prüfsumme erhält und prüft, ob alle Daten im Feld gültig sind. Die Funktion soll true zurückliefern, wenn die Daten gültig sind und ansonsten false.
Legen Sie in main() ein beliebig großes unsigned char-Feld an, das mit Zufallszahlen zu füllen ist.
Berechnen Sie die Prüfsumme für dieses Feld und überprüfen anschließend die Daten auf ihre Gültigkeit.
Ändern Sie das erste Feldelement des Datenfeldes und prüfen erneut die Daten auf ihre Gültigkeit.
Daten sind in Ordnung.
Daten sind fehlerhaft!