C++20: Mehr Details zu Modulen

Modernes C++  –  10 Kommentare

Der letzter Blogbeitrag führte Module ein, die C++20 erhalten wird. Nun wird gezeigt, wie sich bestehende Module verwenden lassen.

Bevor ich den Artikel beginne, möchte ich zum Anschluss den letzten Artikels zu Modulen kurz zusammenfassen.

Eine kompakte Wiederholung

Ich erzeugte im letzten Artikel das Modul math1, das aus einer Module-Interface-Unit und einer Module-Implementation-Unit bestand. Es wurde von einem Client verwendet. Dies sind die drei Dateien.

Module-Interface-Unit
// math1.cppm

export module math1;

export int add(int fir, int sec);
Module-Implementation-Unit
// math1.cpp

module math1;

int add(int fir, int sec){
return fir + sec;
}
Client
// main1.cpp

import math1;

int main(){

add(2000, 20);

}

Das Programm hatte ich mit einem aktuellen Clang- und cl.exe-Compiler übersetzt. Von nun an werde ich mich auf den cl.exe-Compiler einschränken, denn die Kommandozeile zum Übersetzen des Sourcecodes ist kürzer zu schreiben als die des Clang-Compilers. Wie ich in meinen letzten Artikel angekündigt habe, möchte ich nun die Ausgabe des Programms vorstellen.

Verwendung eins Standard-Moduls

Im Wesentlichen hat sich weder die Module-Interface- noch die Module-Implementation-Unit im Modul math2 geändert.

Module-Interface-Unit
// math2.cppm

export module math2;

export int add(int fir, int sec);
Module-Implementation-Unit
// math2.cpp

module math2;

int add(int fir, int sec){
return fir + sec;
}
Client
// main2.cpp

//#include <iostream>

import std.core;

import math2;

int main(){

std::cout << std::endl;

std::cout << "add(2000, 20): " << add(2000, 20) << std::endl;

}

Dank des Moduls std.core kann ich das Ergebnis der Addition ausgeben.

Natürlich hätte ich auch die Header-Datei <iostream> verwenden können. Ich ahne die Frage schon: Welche Module stehen bereits zur Verfügung? Hier ist die Antwort direkt aus dem Artikel "Using C++ Modules in Visual Studio 2017" des Microsoft C++-Team-Blogs.

C++ Modules in Visual Studio 2017

  • std.regex bietet den Inhalt der Header-Datei <regex> an.
  • std.filesystem bietet den Inhalt der Header-Datei <experimental/filesystem> an.
  • std.memory bietet den Inhalt der Head-Drdatei <memory> an.
  • std.threading bietet den Inhalt der Header-Dateien <atomic>, <condition_variable>, <future>, <mutex>, <shared_mutex> und <thread> an.
  • std.core bietet den Rest der C++-Standard-Bibliothek an.

Module stellen eine höhere Abstraktion als Header-Dateien dar. Daher sind sie angenehm zu verwenden. Zusätzlich lässt sich explizit angeben, welcher Name eines Moduls exportiert werden soll.

Exportieren oder nicht exportieren

Das nächste Modul math3 ist ein wenig komplizierter als der vorherige. Hier ist das Interface:

Module-Interface-Unit
// math3.cppm

import std.core;

export module math3;

int add(int fir, int sec);

export int mult(int fir, int sec);

export void doTheMath();

Es enthält die exportierende Modul-Deklaration: export module math3;. Hier genau beginnt die Modul-Deklaration. Formal auch module purview genannt. Nur Namen, die nach der module purview mit export deklariert werden, werden exportiert. Falls nicht, sind die Namen außerhalb des Moduls nicht sichtbar und besitzen daher Modul-Bindung. Dies gilt insbesondere für die Funkion add, aber nicht für die Funktionen mult und doTheMath.

Module-Implementation-Unit
// math3.cpp

module math3;

int add(int fir, int sec){
return fir + sec;
}

int mult(int fir, int sec){
return fir * sec;
}

void doTheMath(){
std::cout << "add(2000, 20): " << add(2000, 20) << std::endl;
}

Ich habe nichts zu der Module-Imlementation-Unit hinzuzufügen. Das Hauptprogramm ist deutlich interessanter.

Client
// main3.cpp

// #include <iostream>
// #include <numeric>
// #include <string>
// #include <vector>
import std.core;

import math3;

int main(){

std::cout << std::endl;

// std::cout << "add(2000, 20): " << add(2000, 20) << std::endl;

std::vector<int> myVec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

std::string doc = "std::accumulate(myVec.begin(), myVec.end(), mult): ";
auto prod = std::accumulate(myVec.begin(), myVec.end(), 1, mult);

std::cout << doc << prod << std::endl;

doTheMath();

}

Das kleine Programm zeigt bereits, dass Module angenehm anzuwenden sind. Anstelle der vier Headerdateien in den Zeilen (1) reicht ein einfacher import std.core Aufruf in der Zeile 2 aus. Das war es bereits. Hier ist die Ausgabe des Programms.

Nun drängt sich natürlich die Frage auf: Was passiert, wenn ich die Zeile (3) verwende. Zur Erinnerung, die Funktion add wird nicht exportiert und besitzt daher Modul-Bindung.

Der Compiler beschwert sich in diesem Fall, dass die Funkiton add verwendet wird, ihr Name aber nicht bekannt ist.

Weitere Details

Zuerst einmal gibt es mehrere Möglichkeiten, Namen eines Moduls zu exportieren.

Export

Das Exportieren von Namen mit dem Export-Spezifizierer wie im Module math3.cppm ist aufwendig:

  • Export-Spezifier
// math3.cppm

import std.core;

export module math3;

int add(int fir, int sec);

export int mult(int fir, int sec);

export void doTheMath()

Anstelle des Export-Specifiers lässt sich auch eine Exported-Gruppe verwenden:

  • Exported-Gruppe
// math3.cppm

import std.core;

export module math3;

int add(int fir, int sec);

export {

int mult(int fir, int sec);
void doTheMath();

}

Die dritte Variation ist es, einen Exported-Namensraum einzusetzen:

  • Exported-Namensraum
// math3.cppm

import std.core;

export module math3;

namespace math3 {

int add(int fir, int sec);

}

export namespace math3 {

int mult(int fir, int sec);
void doTheMath();

}

Alle drei Variationen sind äquivalent.

Oft ist es praktisch, ein Modul zu reexportieren.

Ein Modul reexportieren

Manchmal möchtest du etwas aus einem Modul exportieren, dass du selbst importiert hast. Falls du das importierte Modul nicht exportierst, besitzt dieses konsequenterweise Modul-Bindung und seine Namen sind nicht außerhalb des Moduls sichtbar. Jetzt kommt ein konkretes Beispiel:

  • Sichtbar versus nicht sichtbar

Stelle dir vor, ich möchte die Module math.core und math.core2 in dem neuen Modul math verwenden. Hier sind die Module-Interface-Units der Module math.core und math.core2.

  • Die reexportierten Module
// module interface unit of math.core

export math.core

export int mult(int fir, int sec);
// module interface unit of math.core2

export math.core2

export int add(int fir, int sec);

Weiter geht es. Dies ist da neue Modul math.

  • Das neue Modul math
// module interface unit of math

export module math;

import math.core; // not exported with mult
export import math.core2; // exported with add


// module implementation unit of math

mult(1100, 2); // fine
add(2000, 20); // fine

Wie es das Beispiel schön zeigt, ist es durchaus möglich; exportierte und nichtexportierte Namen im Modul math zu verwenden. Aber das Modul math.core wird nicht exportiert. Nur ein Client, der das Module math nutzt, bemerkt den Unterschied.

  • Client
// Client

import math

mult(1100, 2); // ERROR
add(2000, 20); // fine

Die Funktion mult besitzt Modul-Bindung und ist daher nicht außerhalb des Moduls sichtbar. Lediglich die Funktion add ist sichtbar.

  • Module umpacken

C++20 bietet eine komfortable Art, Module umzupacken. Stecke sie einfach in eine exportierende Gruppe:

export module math;

export{

import math.core;
import math.core2;
import math.basics;

}

Dadurch werden alle Namen für einen Client sichtbar, der das Modul math importiert.

Wie geht's weiter?

Mit meinem nächsten Artikel beginne ich das letzte Hauptkapitel der C++ Core Guidelines: Regeln zur Standard Library. Glaube es oder auch nicht, viele professionelle C++-Entwickler setzen nicht die Standard Template Library (STL) ein. Dies gilt insbesondere für die Algorithmen der STL.