Programmiersprache C++23: Was sonst noch Interessantes im Standard dabei ist

Ich habe bereits 10 Artikel über C++23 geschrieben. Heute möchte ich auf die Features eingehen, die ich in der ersten Runde vergessen habe.​

In Pocket speichern vorlesen Druckansicht

(Bild: SergioVas/Shutterstock)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

C++23 hat einige wichtige Features, aber auch bei den Details gibt es spannende Ergänzungen.

Zur Erinnerung: Hier sind die wichtigsten Features von C++23.

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++.

C++23 bietet mit Deducing this ein kleines, aber sehr wirkungsvolles Feature der Kernsprache. Deducing this ermöglicht es, ähnlich wie in Python den implizit übergebenen Zeiger in einer Memberfunktionsdefinition explizit zu machen. Dank dieser Ableitung werden einige komplexe Techniken in C++, wie CRTP oder das Overload Pattern, zu einem Kinderspiel.

Die C++23-Bibliothek erhält viele wichtige Ergänzungen. Man kann die Standardbibliothek direkt mit import std; importieren oder die C++20-Formatstrings in std::print und std::println anwenden. Außerdem erhalten wir aus Gründen der Performanz flache assoziative Container wie std::flat_map. std::flap_map ist ein Drop-in-Ersatz für std::map. std::optional wird aus Gründen der Kompatibilität um eine monadische Schnittstelle erweitert. Der neue Datentyp std::expected hat bereits eine komponierbare Schnittstelle und kann einen erwarteten oder einen unerwarteten Wert zur Fehlerbehandlung speichern. Dank std::mdspan erhalten wir eine multidimensionale Span. std::generator schließlich ist die erste konkrete Coroutine zur Erzeugung eines Zahlenstroms. std::generator ist Teil der ranges-Bibliothek, die in C++23 ebenfalls verbessert wird.

Mehr Details findet sich in meinen früheren Artikel:

Kernsprache

  1. C++23: Deducing This erstellt explizite Zeiger
  2. C++23: Syntactic Sugar with Deducing This
  3. C++23: Die kleinen Perlen in der Kernsprache
  4. C++23: Mehr kleine Perlen in der Kernsprache

Bibliothek

  1. C++23: Ein modularisierte Standardbibliothek und zwei neue Funktionen
  2. Ranges: Verbesserungen mit C++23
  3. C++23: Eine neue Art der Fehlerbehandlung mit std::expected
  4. C++23: Vier neue assoziative Container
  5. C++23: Eine multidemensionale View

Doch das ist noch nicht alles, was C++23 zu bieten hat.

Bislang unterstützt C++ die folgenden Datentypen:

  • kein Suffix: double
  • f oder F Suffix: float
  • l oder L Suffix: long double

C++23 bringt fünf neue Literal-Suffixe. Die Tabelle auf cppreference zeigt ihre Datentypen und ihre Eigenschaften:

Die neuen Datentypen sind in de Header <stdfloat> definiert. Mit einem vordefinierten Makro lässt sich prüfen, ob die eigene Implementierung die neuen Datentypen unterstützt.

Die Analyse des aktuellen Stacks ist oft sehr nützlich für die Fehlersuche. Die neue Stacktrace-Bibliothk bietet dies genau an.

Die Bibliothek besteht aus zwei Klassen:

  • stacktrace_entry: Darstellung einer Auswertung in einem Stacktrace
  • basic_stacktrace: ein Schnappschuss des gesamten Stacktrace oder eines bestimmten Teils

In einfachen Worten ausgedrückt: Mit der Klasse stacktrace_entry kann man Informationen über eine Auswertung in einem Stacktrace erhalten. Jedes stacktrace_entry-Objekt ist entweder leer oder repräsentiert eine Auswertung in einem Stacktrace.

Die Klasse basic_stacktrace ist ein Schnappschuss des gesamten Stacktrace oder eines bestimmten Teils davon.

Immer noch verwirrend?

Im folgenden einfachen Beispiel ruft die main Funktion die Funktion func1 auf, func1 ruft func2 auf und func2 ruft func3 auf.

// stacktrace1.cpp

#include <iostream>
#include <stacktrace>

 
void func3() {
    std::cout << std::stacktrace::current()  << '\n';
}
 
void func2() {
    func3();
}
 
void func1() {
    func2(); 
}
 
int main() {
    func1();
}

func3 ruft die statische Funktion current auf std::stacktrace auf. std::stacktrace ist ein Alias für std::basic_stacktrace mit dem Standard-Allokator.

Das Programm gibt beim Ausführen Informationen über den Stacktrace.

Das Kompilieren des Programms mit dem Compiler Explorer war eine ziemliche Herausforderung. Es hat eine Weile gedauert, bis ich die passende GCC-Version und die Bibliotheken herausgefunden hatte.

Hier ist die Befehlszeile für g++ 13.1: g++ -std=c++23 -lstdc++_libbacktrace.

Dank des std::stacktrace_entry lässt sich der Stacktrace abfragen.

// stacktrace2.cpp

#include <iostream>
#include <stacktrace>

 
void func3() {
  auto stacktrace = std::stacktrace::current();
  for (const auto& entry: stacktrace) {
    std::cout << "Description: " << entry.description() << '\n';
    std::cout << "file: " << entry.source_file() 
              << " and line: " << entry.source_line() <<'\n';
    std::cout << '\n';
  }
}
 
void func2() {
    func3();
}
 
void func1() {
    func2(); 
}
 
int main() {
    func1();
}

In der Funktion func3 durchlaufe ich die Stack-Trace-Einträge und zeige explizit ihre Beschreibung, Datei und Zeile an.

Über die übrigen Features von C++23 werde ich nur ein paar Worte schreiben. Die Beispiele stammen von cppreference.com.

Beginnen möchte ich mit zwei Features, die sich auf die Performanz eines Programms auswirken können.

Man kann std::unreachable verwenden, um Code zu markieren, der nicht erreicht werden kann. Der Aufruf von std::unreachable ergibt undefined behavior.

// from https://en.cppreference.com/w/cpp/utility/unreachable

switch (xy)
{
case 128: [[fallthrough]];
case 256: [[fallthrough]];
case 512: /* ... */
    tex.clear();
    tex.resize(xy * xy, Color{0, 0, 0, 0});
    break;
default:
    std::unreachable();
}

Mit C++11 gibt es std::function. std::function ist ein polymorpher Funktionswrapper und kann sie beliebige Callables annehmen und ihnen einen Namen geben. Callables sind alle Entitäten, die sich wie Funktionen verhalten, wie Funktionen, Funktionsobjekte oder Lambdas.

Mehr über std::function erfährst du hier: Funktional in TR1 und C++11.

Im Gegensatz zu std::function kann std::move_only_function nur ein konstruierbares Callable bekommen. Das Callable-Objekt kann in der std::move_only_function gespeichert werden, um zusätzliche Speicheranforderungen zu vermeiden.

// from https://en.cppreference.com/w/cpp/utility/functional/move_only_function


auto lambda = [task = std::move(packaged_task)]() mutable { task(); };
 
//  std::function<void()> function = std::move(lambda); // Error
std::move_only_function<void()> function = std::move(lambda); // OK
 

std::byteswap vertauscht die Bytes im einem Wert n. n muss integral sein.

Das folgende Programm wendet std::byteswap an.

// from https://en.cppreference.com/w/cpp/numeric/byteswap

#include <bit>
#include <concepts>
#include <cstdint>
#include <iomanip>
#include <iostream>
 
template<std::integral T>
void dump(T v, char term = '\n')
{
    std::cout << std::hex << std::uppercase << std::setfill('0')
              << std::setw(sizeof(T) * 2) << v << " : ";
    for (std::size_t i{}; i != sizeof(T); ++i, v >>= 8)
        std::cout << std::setw(2) 
                  << static_cast<unsigned>(T(0xFF) & v) << ' ';
    std::cout << std::dec << term;
}
 
int main()
{
    static_assert(std::byteswap('a') == 'a');
 
    std::cout << "byteswap for U16:\n";
    constexpr auto x = std::uint16_t(0xCAFE);
    dump(x);
    dump(std::byteswap(x));
 
    std::cout << "\nbyteswap for U32:\n";
    constexpr auto y = std::uint32_t(0xDEADBEEFu);
    dump(y);
    dump(std::byteswap(y));
 
    std::cout << "\nbyteswap for U64:\n";
    constexpr auto z = std::uint64_t{0x0123456789ABCDEFull};
    dump(z);
    dump(std::byteswap(z));
}

Zum Schluss ist hier die Ausgabe des Programms.

In meinem nächsten Artikel gehe ich auf C++26 ein. (rme)