C++20: Überblick zur Bibliothek

Modernes C++  –  81 Kommentare

Mein letzter Artikel "C++20: Überblick zur Kernsprache" stellte kurz und kompakt die neuen Features der C++20-Kernsprache vor. Heute geht meine Geschichte zu C++20 mit dem Überblick der Bibliothek weiter.

Wie gewohnt zeigt die Grafik, um welche Feature sich der heutige Artikel dreht:

Kalender und Zeitzonen

Die Chrono-Bibliothek von C++11/14 wurde um einen Kalender und Zeitzonen erweitert.

  • Kalender: Er besteht aus Datentypen, die ein Jahr, einen Monat, ein Wochentag und den n-ten Tag der Woche repräsentieren. Diese elementaren Datentypen können zu komplexen Datentypen wie year_month, year_month_day, year_month_day_last, years_month_weekday und year_month_weekday_last verknüpft werden. Der "/" Operator erlaubt den einfache Umgang mit Zeitpunkten. Zusätzlich gib es noch zwei neue Literale in C++20: d für einen Tag und y für ein Jahr.
  • Zeitzonen: Zeitpunkte lassen sich in verschiedenen Zeitzonen darstellen.

Dank der erweiterten Chrono-Bibliothek, lassen sich die folgenden Anwendungen umsetzen:

  • ein Datum in verschiedenen Formaten repräsentieren:
auto d1 = 2019y/oct/28;
auto d2 = 28d/oct/2019;
auto d3 = oct/28/2019;
  • bestimme den letzten Tag eines Monats
  • bestimme die Anzahl der Tage zwischen zwei Daten
  • die Ausgabe der aktuellen Zeit in verschiedenen Zeitzonen

Um erste Erfahrung mit der erweiterten Bibliothek zu sammeln, bietet sich Howard Hinnards Implementierung auf GitHub an. Er ist der Autor des Proposals für die Kalender- und Zeitzonenerweiterung. Auf Wandbox hat er eine Spielwiese für die neuen Features erzeugt:

#include "date.h"
#include <iostream>

int
main()
{
using namespace date;
using namespace std::chrono;
auto now = system_clock::now();
std::cout << "The current time is " << now << " UTC\n";
auto current_year = year_month_day{floor<days>(now)}.year();
std::cout << "The current year is " << current_year << '\n';
auto h = floor<hours>(now) - sys_days{jan/1/current_year};
std::cout << "It has been " << h << " since New Years!\n";
}

Es ist offensichtlich, C++20 wird den Namensraum std::chrono anstelle des Namensraums date verwenden. Hier ist die Ausgabe des Programms:

std::span

std::span steht für ein Objekt, das sich auf einen zusammenhängende Sequenz von Objekten bezieht. Ein std::span, manchmal auch View genannt, ist niemals ein Besitzer. Der zusammenhängende Speicherbereich kann ein Array, ein Zeiger mit einer Länge oder ein std::vector sein. Eine typische Implementierung eines std::span benötigt einen Zeiger auf das erste Element der Sequenz und seine Länge. Der wichtigste Grund, dass std::span im C++20 Standards enthalten ist, ist der Tatsache geschuldet, dass ein C-Array zu einem Zeiger vereinfacht wird (decay), wenn dieses an eine Funktion übergeben wird. Daher bestimmt std::span<T> automatisch die Länge eines einfaches C-Arrays oder eines std::vector. Um einen Zeiger zum Initialisieren eines std::span<T> zu verwenden, benötigt der Konstruktor seine Länge:

template <typename T>
void copy_n(const T* p, T* q, int n){}

template <typename T>
void copy(std::span<const T> src, std::span<T> des){}

int main(){

int arr1[] = {1, 2, 3};
int arr2[] = {3, 4, 5};

copy_n(arr1, arr2, 3); // (1)
copy(arr1, arr2); // (2)

}

Im Unterschied zu der Funktion copy_n (1) benötigt die Funktion copy (2) nicht die Anzahl der Elemente. Daher wird eine große Fehlerquelle mit std::span<T> beseitigt.

constexpr-Container

Das constexpr-Schlüsselwort wird immer dominanter in C++. Zum Beispiel werden viele Algorithmen der Standard Template Library eine neue Überladung mit constexpr erhalten. Es steht für eine Funktion oder ein Funktions-Template und bedeutet, dass die Funktion oder das Funktions-Template potenziell zur Compilezeit ausgeführt werden können. Natürlich stellt sich jetzt die Frage, welche Container zur Compilezeit verwendet werden können? Mit C++20 heißt die Antwort std::string und std::vector.

Vor C++20 konnten beide Container nicht in einem Kontext verwendet werden, der constexpr-Container voraussetzt, da beide drei Einschränkungen besaßen:

  1. Destruktoren konnten nicht constexpr sein.
  2. Dynamische Memory-Allokation war zur Compilezeit nicht möglich.
  3. Das Erzeugen von Objekten an Ort und Stelle mit placement-new war nicht möglich.

Diese Einschränkungen sind mit C++20 verschwunden.

Der Punkt 3 bezieht sich auf das relativ unbekannte placement-new. Es wird gerne verwendet, um ein Objekt in einem vorreservierten Speicherbereich zu instanziieren. Darüber hinaus kann placement-new global und für eigene Datentypen überladen werden.

char* memory = new char[sizeof(Account)];        // allocate memory
Account* account = new(memory) Account; // construct in-place
account->~Account(); // destruct
delete[] memory; // free memory

Diese Schritte sind notwendig für placement-new. In der ersten Zeile wird der Speicher für Account angefordert, der in der folgenden Zeile direkt verwendet wird. Zugegeben, der Ausdruck account->~Account schaut befremdlich aus. Dieser Aufruf stellt eine der Ausnahmen dar, in der der Anwender den Konstruktor direkt aufrufen muss. Zum Abschluss gibt die letzte Zeile den Speicher wieder frei.

Ich werde nicht tiefer in die Details zu constexpr-Container abtauchen. Falls du mehr Details lesen willst, hier ist das Proposal 784R1.

std::format

cppreference.com beschreibt kurz und bündig die neue Bibliothek std::format: "The text formatting library offers a safe and extensible alternative to the printf family of functions. It is intended to complement the existing C++ I/O streams library and reuse some of its infrastructure such as overloaded insertion operators for user-defined types." Diese Beschreibung umfasst ein einfaches Beispiel:

std::string message = std::format("The answer is {}.", 42);

Diese Syntax erinnert mich sehr an die Formatstrings von Python. Es gibt bereits eine Implementierung von std::format auf Github: fmt. Gerne möchte ich noch ein paar Beispiele aus der erwähnten Implementierung vorstellen. In den Beispiel kommt der Namensraum fmt anstelle des Namensraums std zum Einsatz.

  • Formatiere und verwende Positionsargumente:
std::string s = fmt::format("I'd rather be {1} than {0}.", "right", "happy");
// s == "I'd rather be happy than right."
  • Konvertiere eine Ganzzahl auf sichere Weise in einen String:
fmt::memory_buffer buf;
format_to(buf, "{}", 42); // replaces itoa(42, buffer, 10)
format_to(buf, "{:x}", 42); // replaces itoa(42, buffer, 16)
// access the string with to_string(buf) or buf.data()
  • Formatiere benutzerdefinierte Datentypen:
struct date {
int year, month, day;
};

template <>
struct fmt::formatter<date> {
template <typename ParseContext>
constexpr auto parse(ParseContext &ctx) { return ctx.begin(); }

template <typename FormatContext>
auto format(const date &d, FormatContext &ctx) {
return format_to(ctx.out(), "{}-{}-{}", d.year, d.month, d.day);
}
};

std::string s = fmt::format("The date is {}", date{2012, 12, 9});
// s == "The date is 2012-12-9"

Wie geht's weiter?

Wie versprochen, werde ich mich in weiteren Artikeln genauer mit der C++20-Bibliothek beschäftigen. Zuerst möchte ich aber meinen Überblick zu C++20 abschließen. Der nächste Artikel setzt sich mit den neuen Featuren rund um Concurrency auseinander.