Ein Überblick über C++26: die arithmetische Erweiterung in der Bibliothek

Verglichen mit den bisherigen Versionen der Programmiersprache gewinnt C++26 deutlich an mathematischen Fähigkeiten hinzu.

In Pocket speichern vorlesen Druckansicht 5 Kommentare lesen
Ein Abakus auf einem Blatt mit Zahlen

(Bild: Alexander Lukatskiy/Shutterstock.com)

Lesezeit: 4 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Nach meinem Überblick zu den neuen Features in der Bibliothek von C++26 stelle ich im zweiten Beitrag nun die arithmetische Erweiterung kurz vor.

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

Neu in C++26 ist der Subspan std::submdspan. Er ist ein Subspan des bereits vorhandenen Spans std::mdspan (C++23), der es selbst nicht in C++23 geschafft hatte.

Bevor ich mit C++26 fortfahre, muss ich daher einen kurzen Abstecher zu C++23 machen.

Ein std::mdspan ist eine nicht besitzende mehrdimensionale View einer zusammenhängenden Folge von Objekten. Die zusammenhängende Folge von Objekten kann ein einfaches C-Array, ein Zeiger mit einer Größe, ein std::array oder ein std::string sein. Oft wird diese mehrdimensionale View als mehrdimensionales Array bezeichnet.

Die Anzahl der Dimensionen und die Größe jeder Dimension bestimmen die Form des mehrdimensionalen Arrays. Die Anzahl der Dimensionen wird als Rang bezeichnet, die Größe jeder Dimension als Extension. Die Größe des std::mdspan ist das Produkt aller Dimensionen, die nicht 0 sind. Auf die Elemente eines std::mdspan kann mit dem mehrdimensionalen Indexoperator [] zugegriffen werden.

Jede Dimension eines std::mdspan kann eine static oder dynamic extent haben. static extent bedeutet, dass ihre Länge zur Compilezeit angegeben wird; dynamic extent bedeutet, dass ihre Länge zur Laufzeit angegeben wird.

Dank der Klassen-Template-Argument-Ableitung (CTAG) in C++17 kann der Compiler die Template-Argumente oft automatisch aus den Datentypen der Initialisierungen ableiten:

// mdspan.cpp

#include <mdspan>
#include <iostream>
#include <vector>

int main() {
    
    std::vector myVec{1, 2, 3, 4, 5, 6, 7, 8};          // (1)

    std::mdspan m{myVec.data(), 2, 4};                  // (2)
    std::cout << "m.rank(): " << m.rank() << '\n';      // (4)

    for (std::size_t i = 0; i < m.extent(0); ++i) {     // (6)
        for (std::size_t j = 0; j < m.extent(1); ++j) { // (7)
            std::cout << m[i, j] << ' ';                // (8)
        }
        std::cout << '\n';
    }

    std::cout << '\n';

    std::mdspan m2{myVec.data(), 4, 2};                 // (3)
    std::cout << "m2.rank(): " << m2.rank() << '\n';    // (5)

    for (std::size_t i = 0; i < m2.extent(0); ++i) {
        for (std::size_t j = 0; j < m2.extent(1); ++j) {
        std::cout << m2[i, j] << ' ';  
    }
    std::cout << '\n';
  }

}

In diesem Beispiel wende ich die Argumentdeduktion von Klassen-Templates dreimal an. In Zeile (1) wird sie für einen std::vector verwendet, in den Zeilen (2) und (3) für einen std::mdspan. Das erste zweidimensionale Array m hat die Form (2, 4), das zweite m2 die Form (4, 2). In den Zeilen (4) und (5) werden die Ränge beider std::mdspan angezeigt. Dank der Ausdehnung jeder Dimension (Zeilen 6 und 7) und des Indexoperators in Zeile (8) ist es einfach, durch mehrdimensionale Arrays zu iterieren.

Hier endet mein Exkurs zu C++23 und ich fahre in C++26 mit std::submdspan fort.

Die Funktion std::submdspan wurde ursprünglich als entscheidend für die Gesamtfunktionalität von mdspan angesehen. Aufgrund von Zeitbeschränkungen bei der Überprüfung wurde sie jedoch zunächst wieder entfernt, damit mdspan in C++23 aufgenommen werden konnte.

Das Erstellen eines std::submdspan ist unkompliziert. Der erste Parameter ist ein mdspan x, und die übrigen x.rank()-Parameter sind Slice-Spezifizierer, einer für jede Dimension von x. Die Slice-Spezifizierer beschreiben, welche Elemente des Bereichs [0,x.extent(d)) Teil des mehrdimensionalen Indexraums des zurückgegebenen mdspan sind.

Dies führt zu der folgenden grundlegenden Signatur:

template<class T, class E, class L, class A,
         class ... SliceArgs)
auto submdspan(mdspan<T,E,L,A> x, SliceArgs ... args);

Dabei muss E.rank() gleich sizeof...(SliceArgs) sein.

Das Proposal P2630R4 enthält neben der Definition eines std::submdspan auch einige Beispiele für ein mdspan mit Rang 1.

int* ptr = ...;
int N = ...;
mdspan a(ptr, N);

// subspan of a single element
auto a_sub1 = submdspan(a, 1);
static_assert(decltype(a_sub1)::rank() == 0);
assert(&a_sub1() == &a(1));

// subrange
auto a_sub2 = submdspan(a, tuple{1, 4});
static_assert(decltype(a_sub2)::rank() == 1);
assert(&a_sub2(0) == &a(1));
assert(a_sub2.extent(0) == 3);

// subrange with stride
auto a_sub3 = submdspan(a, strided_slice{1, 7, 2});
static_assert(decltype(a_sub3)::rank() == 1);
assert(&a_sub3(0) == &a(1));
assert(&a_sub3(3) == &a(7));
assert(a_sub3.extent(0) == 4);

// full range
auto a_sub4 = submdspan(a, full_extent);
static_assert(decltype(a_sub4)::rank() == 1);
assert(a_sub4(0) == a(0));
assert(a_sub4.extent(0) == a.extent(0));

Die gleichen Regeln gelten für den mehrdimensionalen Anwendungsfall:

int* ptr = ...;
int N0 = ..., N1 = ..., N2 = ..., N3 = ..., N4 = ...;
mdspan a(ptr, N0, N1, N2, N3, N4);

auto a_sub = submdspan(a,full_extent_t(), 3, strided_slice{2,N2-5, 2}, 4, tuple{3, N5-5});

// two integral specifiers so the rank is reduced by 2
static_assert(decltype(a_sub) == 3);
// 1st dimension is taking the whole extent
assert(a_sub.extent(0) == a.extent(0));
// the new 2nd dimension corresponds to the old 3rd dimension
assert(a_sub.extent(1) == (a.extent(2) - 5)/2);
assert(a_sub.stride(1) == a.stride(2)*2);
// the new 3rd dimension corresponds to the old 5th dimension
assert(a_sub.extent(2) == a.extent(4)-8);

assert(&a_sub(1,5,7) == &a(1, 3, 2+5*2, 4, 3+7));

Dies ist allerdings noch nicht das Ende der Unterstützung für C++26.

linalg ist eine auf BLAS (Basic Linear Algebra Subprograms) basierende, freie Schnittstelle für lineare Algebra. BLAS ist laut Wikipedia eine Spezifikation, die eine Reihe von Low-Level-Routinen zur Durchführung gängiger linearer Algebra-Operationen wie Vektoraddition, skalare Multiplikation, Punktprodukte, Linearkombinationen und Matrixmultiplikation vorschreibt. Sie sind de facto der Standard für Low-Level-Routinen in Bibliotheken der linearen Algebra.

Das Proposal P1673R13 schlägt eine Schnittstelle für die Dense Linear Algebra der C++-Standardbibliothek vor, die auf den Dense Basic Linear Algebra Subroutines (BLAS) basiert. Dies entspricht einer Teilmenge des BLAS-Standards.

Diese lineare Algebra wurde in der C++-Community lange vermisst. Gemäß dem Proposal ist der folgende Codeausschnitt das "Hallo Welt" der linearen Algebra. Er skaliert die Elemente eines 1-D-mdspan um einen konstanten Faktor, zuerst sequenziell, dann parallel:

  constexpr size_t N = 40;
  std::vector<double> x_vec(N);

  mdspan x(x_vec.data(), N);
  for(size_t i = 0; i < N; ++i) {
    x[i] = double(i);
  }

  linalg::scale(2.0, x); // x = 2.0 * x
  linalg::scale(std::execution::par_unseq, 3.0, x);
  for(size_t i = 0; i < N; ++i) {
    assert(x[i] == 6.0 * double(i));
  }

Die Neuerungen in C++26 sind damit noch keineswegs abgearbeitet. In meinem nächsten Beitrag schreibe ich über Saturation Arithmetic und die Unterstützung von Concurrency in C++26. (map)