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

C++20: std::span

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.

C++20: 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).

C++20: std::span

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.

C++20: std::span

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.

C++20: std::span

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.

C++20: std::span

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.