C++20: Geschützter Zugriff auf Sequenzen von Objekten mit std::span
Wie lässt sich ein einfaches Array an eine Funktion übergeben? Mit C++20 ist die Antwort ganz einfach: Nutze den std::span-Container.
- Rainer Grimm
In meinen Seminaren gibt es häufig die Diskussion: Wie kann ein einfaches Array an eine Funktion übergeben werden? Mit C++20 ist die Antwort ganz einfach: Verwende den Container std::span
.
std::span
steht für ein Objekt, das sich auf eine zusammenhängende Sequenz von Objekten bezieht. Ein std::span
, manchmal auch View genannt, ist niemals ein Besitzer. Der zusammenhängende Speicherbereich kann ein Array, ein Zeiger mit einer Länge, ein std::vector
oder ein std::string
sein. Eine typische Implementierung eines std::span
benötigt einen Zeiger auf das erste Element der Sequenz und seine Länge. Der wichtigste Grund, weshalb std::span
im C++20 Standards enthalten ist, ist der Tatsache geschuldet, dass ein C-Array zu einem Zeiger vereinfacht wird (decay), wenn dieses an eine Funktion übergeben wird. Dieser decay ist eine typische Ursache für Fehler in C/C++.
Automatische Bestimmung der Länge der kontinuierlichen Sequenz von Objekten
std::span<T>
bestimmt automatisch die Länge eines C-Arrays, eines std::vector
oder eines std::array
.
// 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), der std::vector
(2) und std::array
(3) besitzen int
s. Konsequenterweise erwartet std::span
diese int
s. Das kleine Beispiel verdeutlicht aber einen viel interessanteren Punkt. Für jeden Container kann std::span
seine Länge bestimmen (4).
Alle drei großen C++-Compiler MSVC, GCC und Clang unterstützen bereits std::span
.
Es gibt mehrere Möglichkeiten, einen std::span
zu erzeugen.
Ein std::span
mithilfe eines Zeigers und einer Länge erzeugen
Ein std::span
lässt sich auch mithilfe eines Zeigers und einer Länge erzeugen:
// createSpan.cpp
#include <algorithm>
#include <iostream>
#include <span>
#include <vector>
int main() {
std::cout << std::endl;
std::cout << std::boolalpha;
std::vector myVec{1, 2, 3, 4, 5};
std::span mySpan1{myVec}; // (1)
std::span mySpan2{myVec.data(), myVec.size()}; // (2)
bool spansEqual = std::equal(mySpan1.begin(), mySpan1.end(),
mySpan2.begin(), mySpan2.end());
std::cout << "mySpan1 == mySpan2: " << spansEqual << std::endl; // (3)
std::cout << std::endl;
}
Wie erwartet, besitzen der von einem std::vector
erzeugte mySpan1
(1) und der von einem Zeiger und einer Länge erzeugte mySpan2
(2) den gleichen Inhalt (3).
Gerne wird std::span
auch als View bezeichnet. Verwechsle nicht std::span
mit einem View der Ranges-Bibliothek (C++20) oder einem std::string_view
(C++17).
Eine View der Ranges-Bibliothek lässt sich auf einer Range anwenden. Dabei wird eine Operation ausgeführt. Views besitzen keine Daten. Konsequenterweise sind seine Copy-, Move- oder Zuweisungsoperationen konstant. Eric Niebler, Autor der ranges-v3-Implementierung, die Grundlage für die Ranges-Bibliothek in C++20 ist, beschreibt Ranges mit folgenden Worten: "Views are composable adaptations of ranges where the adaptation happens lazily as the view is iterated." Hier sind alle meine Artikel, die sich mit der Ranges-Bibliothek beschäftigen: Kategorie Ranges-Bibliothek.
Ein View (std::span
) und ein std::string_view
sind nichtbesitzende Views und können mit Strings umgehen. Der entscheidende Unterschied zwischen einem std::span
und einem std::string_view
ist, dass ein std::span
seine referenzierten Objekte verändern kann. Falls du mehr zu std::string_view
lesen willst, hier sind meine älteren Artikel: C++17: Was gibts Neues in der Bibliothek? und C++17: Vermeide Kopieren mit std::string_view.
Die Objekte verändern
Sowohl der ganze std::span
als auch nur Teile von ihm lassen sich verändern. Wenn du den std::span
modifizierst, hat das natürlich auch Ausweirkungen auf seine referenzierten Objekte.
Das folgende Beispiel zeigt, wie sich mithilfe eines Teilbereichs (subspan) die referenzierten Objekte eines std::vector
verändern lassen:
// spanTransform.cpp
#include <algorithm>
#include <iostream>
#include <vector>
#include <span>
void printMe(std::span<int> container) {
std::cout << "container.size(): " << container.size() << std::endl;
for(auto e : container) std::cout << e << ' ';
std::cout << "\n\n";
}
int main() {
std::cout << std::endl;
std::vector vec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printMe(vec);
std::span span1(vec); // (1)
std::span span2{span1.subspan(1, span1.size() - 2)}; // (2)
std::transform(span2.begin(), span2.end(), // (3)
span2.begin(),
[](int i){ return i * i; });
printMe(vec);
}
span1
referenziert den std::vector vec
(1). Im Gegensatz dazu verweist span2
auf die Elemente des zugrundeliegenden vec
. Dabei ignoriert span2
das erste und letzte Elements. Konsequenterweise adressiert die Abbildung jedes Elements auf sein Quadrat (2) nur diese Elemente.
Ein std::span
enthält einige komfortable Funktionen, um auf seine Elemente zuzugreifen.
Zugriff auf die Elemente eines std::span
Die Tabelle stellt kompakt die Zugriffsfunktionen des std::span
vor.
Das folgende Beispiel zeigt die Funktion subspan
in der Anwendung:
// subspan.cpp
#include <iostream>
#include <numeric>
#include <span>
#include <vector>
int main() {
std::cout << std::endl;
std::vector<int> myVec(20);
std::iota(myVec.begin(), myVec.end(), 0); // (1)
for (auto v: myVec) std::cout << v << " ";
std::cout << "\n\n";
std::span<int> mySpan(myVec); // (2)
auto length = mySpan.size();
auto count = 5; // (3)
for (long unsigned int first = 0; first <= (length - count); first += count ) {
for (auto ele: mySpan.subspan(first, count)) std::cout << ele << " ";
std::cout << std::endl;
}
}
Das Programm füllt den Vektor mit den Zahlen von 0 bis 19 (1) und initialisiert einen std::span
mit diesem (2). Der Algorithmus std::iota
füllt den Bereich mit aufeinanderfolgenden Werten, die per Default bei 0 beginnen, auf. Zuletzt setzt die for-Schleife (3) die Funktion subspan
ein, um alle Teilbereiche zu erzeugen, die count
Elemente besitzen und bei first
beginnen. Die Schleife wird so lange ausgeführt, bis mySpan
konsumiert ist.
Wie geht's weiter?
Container der STL werden mit C++20 noch mächtiger. Zum Beispiel lassen sich std::string
und std::vector
zur Compilezeit erzeugen und modifizieren. Darüber hinaus geht dank der Funktionen std::erase
und std::erase_if
das Löschen der Elemente eines Containers deutlich leichter von der Hand.
()