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.
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 [1]), 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 [2] (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 [3]-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 [4].
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? [5] und C++17: Vermeide Kopieren mit std::string_view [6].
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.
( [7])
URL dieses Artikels:
https://www.heise.de/-4892418
Links in diesem Artikel:
[1] http://codeofthedamned.com/index.php/type-decay
[2] https://www.grimm-jaud.de/index.php/blog/tag/ranges-bibliothek
[3] https://github.com/ericniebler/range-v3
[4] https://www.grimm-jaud.de/index.php/blog/tag/ranges-bibliothek
[5] https://www.grimm-jaud.de/index.php/blog/c-17-was-gibts-neues-in-der-bibliothek
[6] https://www.grimm-jaud.de/index.php/blog/c-17-vermeide-kopieren-mit-std-string-view
[7] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien