Ein Überblick über C++26: Zusätzliche Bibliotheksfunktionen und Concurrency

Zum Abschluss des Überblicks über C++26 stehen Sättigungsarithmetik und Debugging sowie die Concurrency im Fokus.

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen

Gleisanlagen in Lübeck.

(Bild: heise online / anw)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Es gibt noch zwei Features in der Bibliothek von C++26, bevor ich mich der Concurrency zuwende: Sättigungsarithmetik und Debugging-Unterstützung.

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

Die englische Wikipedia-Seite beschreibt Sättigunsarithmetik folgendermaßen: Saturation arithmetic is a version of arithmetic in which all operations, such as addition and multiplication, are limited to a fixed range between a minimum and maximum value. If the result of an operation is greater than the maximum, it is set (“clamped“) to the maximum; if it is below the minimum, it is clamped to the minimum. The name comes from how the value becomes “saturated” once it reaches the extreme values; further additions to a maximum or subtractions from a minimum will not change the result.

Mit C++26 wurde eine Reihe von sättigenden arithmetischen Operationen eingeführt: Addition, Subtraktion, Multiplikation, Division und Sättigungsumwandlung. Wenn der angegebene ganzzahlige Datentyp T das Ergebnis der Operation nicht darstellen kann, ist das Ergebnis stattdessen std::numeric_limits::min<T>() oder std::numeric_limits::max<T>() (je nachdem, welcher Wert näher liegt).

cppreference.com hat ein schönes Beispiel mit einer Erklärung zu std::add_sat:

#include <climits>
#include <limits>
#include <numeric>
 
static_assert(CHAR_BIT == 8);
static_assert(UCHAR_MAX == 255);
 
int main()
{
    constexpr int a = std::add_sat(3, 4); // no saturation occurs, T = int
    static_assert(a == 7);
 
    constexpr unsigned char b = std::add_sat<unsigned char>(UCHAR_MAX, 4); // saturated
    static_assert(b == UCHAR_MAX);
 
    constexpr unsigned char c = std::add_sat(UCHAR_MAX, 4); // not saturated, T = int
        // add_sat(int, int) returns int tmp == 259,
        // then assignment truncates 259 % 256 == 3
    static_assert(c == 3);
 
//  unsigned char d = std::add_sat(252, c); // Error: inconsistent deductions for T
 
    constexpr unsigned char e = std::add_sat<unsigned char>(251, a); // saturated
    static_assert(e == UCHAR_MAX);
        // 251 is of type T = unsigned char, `a` is converted to unsigned char value;
        // might yield an int -> unsigned char conversion warning for `a`
 
    constexpr signed char f = std::add_sat<signed char>(-123, -3); // not saturated
    static_assert(f == -126);
 
    constexpr signed char g = std::add_sat<signed char>(-123, -13); // saturated
    static_assert(g == std::numeric_limits<signed char>::min()); // g == -128
}

T ist der Datentyp beider Funktionsargumente:

template< class T >
constexpr T add_sat( T x, T y ) noexcept;

C++26 verfügt über drei Funktionen für das Debugging.

  • std::breakpoint hält das laufende Programm an, wenn es aufgerufen wird, und übergibt die Kontrolle an den Debugger,
  • std::breakpoint_if_debugging ruft std::breakpoint auf, wenn std::is_debugger_present true zurückgibt,
  • std::is_debugger_present prüft, ob ein Programm unter der Kontrolle eines Debuggers ausgeführt wird.

Dies war der erste Überblick über die C++26-Bibliothek. Fahren wir mit Concurrency fort.

Concurrency in C++26 hat ein dominierendes Feature:

std::execution, das zuvor als Senders/Receivers bezeichnet wurde, bietet ein Standard C++Framework zum Verwalten der asynchronen Ausführung auf generischen Ressourcen (P2300R10). Es hat drei wesentliche Elemente: Schedulers, Senders und Receiver und bietet ein Set von anpassbaren asynchronen Algorithmen.

Das Programm „Hello world“ des Proposal P2300R10 zeigt sie:

using namespace std::execution;

scheduler auto sch = thread_pool.scheduler();                                 // 1

sender auto begin = schedule(sch);                                            // 2
sender auto hi = then(begin, []{                                              // 3
    std::cout << "Hello world! Have an int.";                                 // 3
    return 13;                                                                // 3
});                                                                           // 3
sender auto add_42 = then(hi, [](int arg) { return arg + 42; });              // 4

auto [i] = this_thread::sync_wait(add_42).value();  

Die Erklärung des Beispiels ist so gut, dass ich sie hier direkt zitieren werde:

This example demonstrates the basics of schedulers, senders, and receivers:

  1. First we need to get a scheduler from somewhere, such as a thread pool. A scheduler is a lightweight handle to an execution resource.
  2. To start a chain of work on a scheduler, we call § 4.19.1 execution::schedule, which returns a sender that completes on the scheduler. A sender describes asynchronous work and sends a signal (value, error, or stopped) to some recipient(s) when that work completes.
  3. We use sender algorithms to produce senders and compose asynchronous work. § 4.20.2 execution::then is a sender adaptor that takes an input sender and a std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.
  4. Now, we add another operation to the chain, again using § 4.20.2 execution::then. This time, we get sent a value – the int from the previous step. We add 42 to it, and then return the result.
  5. Finally, we’re ready to submit the entire asynchronous pipeline and wait for its completion. Everything up until this point has been completely asynchronous; the work may not have even started yet. To ensure the work has started and then block pending its completion, we use § 4.21.1 this_thread::sync_wait, which will either return a std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stopped signal, or it throws an exception if the last sender sent an error.

Der Proposal enthält weitere hervorragende Beispiele. Ich werde sie in den kommenden Artikeln analysieren.

Read-Copy Update und Hazard Pointers lösen das klassische Problem von Lock-freien Datenstrukturen wie einem Lock-freien Stack: Wann kann ein Thread einen Datenstrukturknoten sicher löschen, während andere Threads diesen Knoten gleichzeitig verwenden können?

Diese Strukturen sind zu speziell und benötigen zu viele Informationen, um sie in einem Übersichtsartikel zu behandeln.

Im nächsten Artikel über C++26 werde ich meinen Fokus ändern und mich mit den Details befassen. (rme)