Softwareentwicklung: std::span in C++20: Weitere Details
In diesem Blogbeitrag geht es um das Objekt std::span, das sich auf eine zusammenhängende Folge von Objekten bezieht, und um ein persönliches Anliegen.

(Bild: SergioVas/Shutterstock)
- Rainer Grimm
Heute möchte ich über die nicht so bekannten Features eines std::span
und die Gefahren schreiben. Außerdem geht es um ein persönliches Anliegen für die Forschung zu ALS.
Ein std::span
, manchmal auch View genannt, ist nie ein Besitzer. Dieser zusammenhängende Speicher kann ein einfaches Array, ein Zeiger mit einer bestimmten Größe, ein std::array,
ein std::vector
oder ein std::string
sein. Eine typische Implementierung besteht aus einem Zeiger auf sein erstes Element und einer Größe. Der Hauptgrund für eine std::span<T>
ist: Ein C-Array wird auf einen Zeiger auf seines erstes Element reduziert wird, wenn es an eine Funktion übergeben wird. Dadurch geht die Größe des Arrays verloren. Dieser decay ist ein typischer Grund für Fehler in C/C++.
Automatisch ermittelte Größe
Im Gegensatz dazu leitet std::span<T>
die Größe von zusammenhängenden Folgen von Objekten automatisch ab.
// printSpan.cpp
#include <iostream>
#include <vector>
#include <array>
#include <span>
void printMe(std::span<int> container) {
std::cout << "container.size(): " << container.size() << '\n'; // (4)
for(auto e : container) std::cout << e << ' ';
std::cout << "\n\n";
}
int main() {
std::cout << std::endl;
int arr[]{1, 2, 3, 4}; // (1)
printMe(arr);
std::vector vec{1, 2, 3, 4, 5}; // (2)
printMe(vec);
std::array arr2{1, 2, 3, 4, 5, 6}; // (3)
printMe(arr2);
}
Das C-Array (1), std::vector
(2) und das std::array
(3) besitzen int
s. Folglich enthält std::span
auch int
s. In diesem einfachen Beispiel gibt es noch etwas Interessantes. FĂĽr jeden Container kann std::span
seine Größe ableiten (4).
Dies war eine kurze Erinnerung an std::span
. Alles Weitere steht in meinem vorherigen Artikel "C++20: GeschĂĽtzer Zugriff auf Sequenzen von Objekte mit std::span".
Eine std::span kann einen static extent oder einen dynamic extent haben.
Statischer versus dynamischer Extent
Standardmäßig hat std::span
einen dynamischen Extent:
template <typename T, std::size_t Extent = std::dynamic_extent>
class span;
Wenn ein std::span
einen statischen Extent besitzt, ist seine Größe zur Compile-Zeit bekannt und Teil des Datentyps: std::span
. Daher braucht ihre Implementierung nur einen Zeiger auf das erste Element der zusammenhängenden Folge von Objekten.
Die Implementierung eines std::span
mit einem dynamischen extent besteht aus einem Zeiger auf das erste Element und der Größe der zusammenhängenden Folge von Objekten. Die Größe ist nicht Teil des std::span
-Typs.
Das nächste Beispiel verdeutlicht die Unterschiede zwischen den beiden Arten von Spans.
// staticDynamicExtentSpan.cpp
#include <iostream>
#include <span>
#include <vector>
void printMe(std::span<int> container) { // (3)
std::cout << "container.size(): " << container.size() << '\n';
for (auto e : container) std::cout << e << ' ';
std::cout << "\n\n";
}
int main() {
std::cout << '\n';
std::vector myVec1{1, 2, 3, 4, 5};
std::vector myVec2{6, 7, 8, 9};
std::span<int> dynamicSpan(myVec1); // (1)
std::span<int, 4> staticSpan(myVec2); // (2)
printMe(dynamicSpan);
printMe(staticSpan);
// staticSpan = dynamicSpan; ERROR // (4)
dynamicSpan = staticSpan; // (5)
printMe(staticSpan); // (6)
std::cout << '\n';
}
dynamicSpan
(1) hat einen dynamischen Extent, während staticSpan
(2) einen statischen hat. Beide std::span
s geben ihre Größe in der Funktion printMe
(3) zurĂĽck. Ein std::span
mit statischen Extent kann einem std::span
mit dynamischen extent zugewiesen werden, aber nicht umgekehrt. (4) wĂĽrde einen Fehler verursachen, aber (5) und (6) sind gĂĽltig.
Es gibt einen besonderen Anwendungsfall von std::span
. Eine std::span
kann ein konstanter Bereich von veränderbaren Elementen sein.
Ein konstanter Bereich von veränderbaren Elementen
Der Einfachheit halber nenne ich einen std::vector
und einen std::span
Bereich. Ein std::vector
modelliert einen veränderbaren Bereich von veränderbaren Elementen: std::vector
. Wenn man diesen std::vector
als const
deklariert, modelliert er einen konstanten Bereich konstanter Objekte: const std::vector
. Man kann keinen konstanten Bereich von veränderbaren Elementen modellieren. An dieser Stelle kommt std::span
ins Spiel. Ein std::span
modelliert einen konstanten Bereich von veränderbaren Objekten: std::span
. Die folgende Abbildung verdeutlicht die Unterschiede zwischen (konstanten/veränderbaren) Bereichen und (konstanten/veränderbaren) Elementen.
// constRangeModifiableElements.cpp
#include <iostream>
#include <span>
#include <vector>
void printMe(std::span<int> container) {
std::cout << "container.size(): " << container.size() << '\n';
for (auto e : container) std::cout << e << ' ';
std::cout << "\n\n";
}
int main() {
std::cout << '\n';
std::vector<int> origVec{1, 2, 2, 4, 5};
// Modifiable range of modifiable elements
std::vector<int> dynamVec = origVec; // (1)
dynamVec[2] = 3;
dynamVec.push_back(6);
printMe(dynamVec);
// Constant range of constant elements
const std::vector<int> constVec = origVec; // (2)
// constVec[2] = 3; ERROR
// constVec.push_back(6); ERROR
std::span<const int> constSpan(origVec); // (3)
// constSpan[2] = 3; ERROR
// Constant range of modifiable elements
std::span<int> dynamSpan{origVec}; // (4)
dynamSpan[2] = 3;
printMe(dynamSpan);
std::cout << '\n';
}
Der Vektor dynamVec
(1) ist ein änderbarer Bereich mit änderbaren Elementen. Diese Feststellung gilt nicht für den Vektor constVec
(2). constVec
kann weder seine Elemente noch seine Größe ändern. constSpan
(3) verhält sich entsprechend. dynamSpan
(4) modelliert den einzigartigen Anwendungsfall eines konstanten Bereichs mit veränderbaren Elementen.
Zum Schluss möchte ich noch auf zwei Gefahren hinweisen, die man beim Verwenden von std::span
kennen sollte.
Gefahren von std::span
Die typischen Probleme von std::span
sind zweierlei. Erstens sollte eine std::span
nicht auf einen temporären Bereich angewandt werden; zweitens sollte die Größe des zugrundeliegenden zusammenhängenden Bereichs einer std::span
nicht verändert werden.
Ein std::spa
n auf ein temporären Bereich
Ein std::span
ist nie ein Besitzer und verlängert die Lebensdauer des zugrundeliegend Bereichs nicht. Daher sollte eine std::span
nur auf lValues operieren. Die Verwendung einer std::span
auf einem temporären Bereich ist undefiniertes Verhalten.
// temporarySpan.cpp
#include <iostream>
#include <span>
#include <vector>
std::vector<int> getVector() { // (2)
return {1, 2, 3, 4, 5};
}
int main() {
std::cout << '\n';
std::vector<int> myVec{1, 2, 3, 4, 5}; // (1)
std::span<int, 5> mySpan1{myVec};
std::span<int, 5> mySpan2{getVector().begin(), 5}; // (3)
for (auto v: std::span{myVec}) std::cout << v << " ";
std::cout << '\n';
for (auto v: std::span{getVector().begin(), 5}) std::cout << v << " "; // (4)
std::cout << "\n\n";
}
Die Verwendung eines std::span
mit einem statischen Extent oder einer std::span
mit einem dynamischen Extent auf dem lValue ist in Ordnung. Wenn ich vom lValue std::vector
in (1) zu einem temporären std::vector
wechsle, der durch die Funktion getVector
(2) erzeugt wird, besitzt das Programm undefiniertes Verhalten. Sowohl (3) als auch (4) sind ungĂĽltig. Beim AusfĂĽhren wird das undefinierte Verhalten sichtbar. Die Ausgabe in (4) stimmt nicht mit dem std::vector
ĂĽberein, der von der Funktion getVector()
erzeugt wird.
Ändern der Größe des zugrundeliegenden zusammenhängenden Bereichs
Wenn man die Größe des zugrundeliegenden zusammenhängenden Bereichs ändert, wird der zusammenhängende Bereich möglicherweise neu allokiert und der std::span
bezieht sich auf veraltete Daten.
std::vector<int> myVec{1, 2, 3, 4, 5};
std::span<int> sp1{myVec};
myVec.push_back(6); // undefined behavior
Wie geht's weiter?
In meinem nächsten Artikel tauche ich noch einmal in die Formatierungsbibliothek von C++20 ein.
Unsere Spendenaktion fĂĽr die ALS Forschung
Mein 3-Bücher-Bundle Modern C++ Collection ist jetzt für die Hälfte des Preises ($70 -> $35) verfügbar. Ich werde das Geld für die ALS-Forschung spenden. Lass mich mit den Fakten beginnen.
Die Forschung rund um ALS ist stark unterfinanziert
Hier ist eine Erklärung der ALS-Ambulanz:
ALS ist eines der “Sorgenkinder” der Medizin – seltene Krankheiten stehen nicht im sozialen Fokus. Es existiert keine generelle Finanzierung der ALS-Forschung. Deshalb ist die Behandlung von Menschen mit ALS erheblich unterfinanziert. Hierbei sind Spenden und externe Unterstützung von großer Wichtigkeit.
Diese Unterfinanzierung ist durch die Ice Bucket Challenge deutlich geworden.
Die Forschung braucht mehr Geld. Hier ist meine Idee. Lasst uns eine Spendenaktion starten!
Unsere Spendenaktion
Der Coupon für das 3-Bücher-Bundle Modern C++ Collection für die Hälfte des Preises ($70 -> $35) ist bis zum 21. Januar gültig (inklusive).
Die folgenden drei BĂĽcher sind in diesem Bundle enthalten: The C++ Standard Library, Concurrency with Modern C++ und C++20.
Ich werde das gesamte Geld an die ALS-Ambulanz spenden. Die ALS-Ambulanz ist Teil der Charité und ist führend in der Erforschung und Behandlung von ALS in Europa. (rme)