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.
- Rainer Grimm
Nach meinem Überblick zu den neuen Features in der Bibliothek von C++26 stelle ich im zweiten Beitrag nun die arithmetische Erweiterung kurz vor.
std::submdspan
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.
std::mdspan
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.
std::submdspan
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> – lineare Algebra
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));
}
Wie geht es weiter?
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)