C++20: Geschützter Zugriff auf Sequenzen von Objekten mit std::span

Modernes C++ Rainer Grimm  –  3 Kommentare

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 ints. Konsequenterweise erwartet std::span diese ints. 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.