C++23: Eine modularisierte Standardbibliothek und zwei neue Funktionen
Dank der modularisierten Standardbibliothek und den beiden Funktionen std::print und std::println lässt sich C++23 deutlich angenehmer verwenden.
- Rainer Grimm
Die C++23-Standardbibliothek glänzt mit beeindruckenden Verbesserungen. In diesem Artikel werde ich über die modularisierte Standardbibliothek und die beiden praktischen Funktionen std::print
und std::println
schreiben.
Jede Programmierherausforderung in einer neuen Sprache beginnt mit dem "Hello World"-Programm. Seit C++98 war dies unser Ausgangspunkt:
#include <iostream>
int main() {
std::cout << "Hello World\n";
}
Offen gestanden musst du deine alten Gewohnheiten in C++23 ablegen. Das "Hello World"-Programm sieht nun folgendermaßen aus:
import std;
int main() {
std::println("Hello World");
}
Lass mich das Programm analysieren.
Modularisierte Standardbibliothek
C++23 unterstützt eine modularisierte Standardbibliothek. Mit dem einfachen Kommando import std
; steht die gesamte Standardbibliothek zu deiner Verfügung. Wenn du auch globale C-Funktionen wie printf
nutzen willst, musst du import std.compat;
verwenden. Hier ist das entsprechende "Hello World"-Programm mit printf:
import std.compat;
int main() {
printf("Hello World\n");
}
Die modularisierte Standardbibliothek bringt zwei wesentliche Verbesserungen mit: eine deutlich verbesserte Compile-Zeit und Usability.
Deutliche verbesserte Compile-Zeit
Der Import der Standardbibliothek (import std
) ist buchstäblich "kostenlos". Damit ist gemeint, dass die Compile-Zeiten deutlich sinken. Die ersten Erfahrungszahlen besagen, dass sich die Compile-Zeiten mindestens um den Faktor 10 verkürzen. Der Grund für diese Verbesserung ist offensichtlich. Anstatt deine Header-Dateien sukzessive zu inkludieren, importierst du ein Modul. Deswegen gibt es nur ein Modul, das die ganze C++23-Standardbibliothek enthält. Bislang unterstützt nur der MSVC-Compiler diese modularisierte Standardbibliothek: Tutorial: Import the C++ standard library using modules from the command line.
Verbesserte Usability
Angenommen, du willst die Funktion std::accumulate
verwenden. Weißt du, welche Headerdatei du einfügen musst? Ist es die Headerdatei <numeric
>, <functional
> oder <algorithm
>? Vielleicht war dies zu einfach für dich. Wie sieht es nun mit std::forward
aus oder warum lässt sich das folgende Programm nicht kompilieren?
int main() {
auto list = {1, 2, 3, 4};
}
Der Ausdruck {1, 2, 3, 4}
ist eine std::initializer_list<int>
. Um sie zu verwenden, muss der Header <initializer_list
> eingesetzt werden. Natürlich wird dieser Header automatisch eingebunden, wenn ein Container wie std::vector
zum Einsatz kommt.
Vergleiche die vorherigen Beispiele mit einem einfachen import std;
oder import std::compat;.
Aus eigener Erfahrung weiß ich: Nicht nur Anfänger scheitern öfter mal beim Einsatz der richtigen Headerdateien.
Ich weiß nicht, ob du es bemerkt hast, aber mein C++23-"Hello World"-Programm hat ein zweites Feature von C++23 verwendet:
std::print
und std::println
C++23 bietet für beide Funktionen zwei Überladungen an:
std::print
template< class... Args >
void print( std::FILE* stream,
std::format_string<Args...> fmt, Args&&... args );
template< class... Args >
void print( std::format_string<Args...> fmt, Args&&... args );
std::println
template< class... Args >
void println( std::FILE* stream,
std::format_string<Args...> fmt, Args&&... args );
template< class... Args >
void println( std::format_string<Args...> fmt, Args&&... args );
Der erste Unterschied zwischen std::print
und std::println
ist offensichtlich: std::println
fügt einen Zeilenumbruch hinzu. Noch interessanter sind hingegen folgende Punkte:
Variadic Template
std::print
und std::println
sind Variadic Templates. Variadic Templates sind Templates, die eine beliebige Anzahl von Argumenten annehmen können. Ihre Argumente werden perfekt weitergeleitet (perfect forwarding). std::print
und std::println
sind die typsichere Variante von printf
. Bei printf
musst du den Formatstring angeben, bei std::print
und std::println
verwendest du Platzhalter im Formatstring. Im Allgemeinen leitet der Compiler den Datentyp für die Platzhalter ab, indem er die Regeln von std::format
aus C++20 anwendet. Folgerichtig scheinen std::print
und std::println
in C++23 Syntactic Sugar für Formatstrings in C++20 zu sein. Hier ist das modifizierte "Hello World"-Programm in C++23, das std::format
verwendet.
import std;
int main() {
// std::println("Hello World");
std::cout << std::format("{:}\n", "Hello World");
}
Wenn du mehr über Variadic Templates, Perfect Forwarding und std::forma
t wissen willst, lies meine älteren Artikel:
Unicode Support
Dass std::print
und std::println
in C++23 Syntactic Sugar für Formatstrings in C++20 zu sein scheinen, stimmt so nicht. Denn std::print
und std::println
unterstützen Unicode. Lasse mich dazu aus dem Proposal P2093R14 zitieren:
Another problem is formatting of Unicode text:
std::cout << "Привет, κόσμος!";
If the source and execution encoding is UTF-8 this will produce the expected output on most GNU/Linux and macOS systems. Unfortunately on Windows it is almost guaranteed to produce mojibake despite the fact that the system is fully capable of printing Unicode, for example
Привет κόσμος!
even when compiled with /utf-8 using Visual C++ ([MSVC-UTF8]). This happens because the terminal assumes code page 437 in this case independently of the execution encoding.
With the proposed paper
std::print("Привет, κόσμος!");
will print "Привет, κόσμος!" as expected allowing programmers to write Unicode text portably using standard facilities.
Beliebiger Ausgabestrom
Beide Varianten std::print
und std:println
besitzen eine Überladung, die einen beliebigen Ausgabestrom akzeptiert. Per Default ist der Ausgabestrom stdout
.
std::format
und folglich auch std::print
und std::println
in C++23 haben noch mehr zu bieten. In C++23 kannst du einen Container der Standard Template Library formatieren.
Formatierte Ausgabe eines Containers
Das folgende Programm zeigt, wie du einen Container der STL direkt ausgeben kannst. Bislang unterstützt kein C++-Compiler diese Funktionalität. Nur der brandneue Clang-Compiler mit der libc++
anstelle der libstdc++
ermöglicht es, diese Funktion zumindest teilweise zu nutzen. In einer vollständig konformen C++23-Implementierung könnte ich std::println
anstelle von std::format
verwenden.
// formatVector.cpp
#include <format>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> myInts{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << std::format("{:}\n", myInts);
std::cout << std::format("{::+}\n", myInts);
std::cout << std::format("{::02x}\n", myInts);
std::cout << std::format("{::b}\n", myInts);
std::cout << '\n';
std::vector<std::string> myStrings{"Only", "for", "testing", "purpose"};
std::cout << std::format("{:}\n", myStrings);
std::cout << std::format("{::.3}\n", myStrings);
}
Ich verwende in diesem Beispiel einen std::vector<int>
und einen std::vector<std::string>
. Wenn {:}
als Platzhalter zum Einsatz kommt, werden beide Container (Zeilen 1 und 2) direkt angezeigt. Werden zwei Doppelpunkte {::}
innerhalb des Platzhalters verwendet, lassen sich die Elemente des std::vector
formatieren. Der Formatspezifizierer folgt auf den zweiten Doppelpunkt.
std::vector<int>
: Die Elemente des Vektors haben ein + Zeichen{::+}
, sind hexadezimal auf 2 Zeichen mit der 0 als Füllzeichen{::02x}
ausgerichtet und werden binär dargestellt{::b}
.std::vector<std::string>
: Jeder String wird auf seine ersten 3 Zeichen gekürzt:{::.3}
.
Der folgende Screenshot zeigt die Ausgabe des Programms im Compiler Explorer:
Wie geht's weiter?
In meinem nächsten Artikel über die verbesserte Standardbibliothek in C++23 werde ich die erweiterte Schnittstelle von std::optional
und den neuen Datentyp std::expected
für die Fehlerbehandlung vorstellen.
(map)