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.

In Pocket speichern vorlesen Druckansicht 37 Kommentare lesen

(Bild: Carlos Amarillo/Shutterstock.com)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

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.

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.

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.

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:

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:

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::format wissen willst, lies meine älteren Artikel:

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.

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.

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:

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)