Iteratoren und Ranges
Ein Iterator dient zum Zugriff auf Elemente in einem Container oder Stream und ist sinngemäß das OOP-Gegenstück zum klassischen Zeiger. Er ist eng verbunden mit dem zu bearbeitenden Container bzw. Stream, d.h., er ist immer Member des jeweiligen Containertyps.
Einzubindende Header-Datei: iterator
Iterator-Kategorien
Input-Iterator:
Erlaubt ausschließlich Lesezugriffe und kann nur inkrementiert werden. Einmal gelesene Elemente können nicht erneut gelesen werden.
Output-Iterator:
Ist das Gegenstück zum Input-Iterator, erlaubt ausschließlich Schreibzugriffe und kann ebenfalls nur inkrementiert werden.
Forward-Iterator:
Ist eine Kombination aus Input- und Output-Iterator. Er kann nur inkrementiert werden, erlaubt aber das Lesen und Schreiben von Daten.
Bidirektionaler Iterator:
Erweitert die Fähigkeiten des Forward-Iterators um die Möglichkeit, dass er sowohl inkrementiert als auch dekrementiert werden kann.
Random-Access Iterator:
Der flexibelste Iterator ist der Random-Access Iterator. Er kann nicht nur inkrementiert und dekrementiert werden, sondern durch eine Addition bzw. Subtraktion eines Datums beliebig positioniert werden.
Da ein Iterator Member des Containertyps ist, ergibt sich automatisch dessen Iterator-Kategorie. So ist z.B. der Iterator eines std::vector immer ein Random-Access Iterator während der Iterator einer std::forward_list immer ein Forward-Iterator ist.
Iterator-Operationen
Die nachfolgenden Tabellen enthalten alle Operationen, die mit dem jeweiligen Iterator möglich sind. In der Tabelle gilt, dass ein Iterator alle Operationen der darunterliegenden Ebene plus seine eigenen Operationen erlaubt, d.h., ein bidirektionaler Iterator enthält alle Operationen eines Forward-Iterators, der wiederum alle Operationen eines Input- und Output-Iterators enthält.
Ebene 1
Input-Iterator
| Operation | Beschreibung |
|---|---|
| *iter | Aktuelles Element auslesen |
| iter->member | Member des aktuellen Elements lesen |
| iter++, ++iter |
Iterator erhöhen |
| iter1 != iter2, iter1 == iter2 |
Iteratoren vergleichen |
| DTYP(iter) | Kopierkonstruktor |
Output-Iterator
| Operation | Beschreibung |
|---|---|
| *iter | Aktuelles Element beschreiben |
| iter->member | Member des aktuellen Elements beschreiben |
| iter++, ++iter |
Iterator inkrementieren |
| iter1 != iter2, iter1 == iter2 |
Iteratoren vergleichen |
| DTYP(iter) | Kopierkonstruktor |
Ebene 2
Forward-Iterator
| Operation | Beschreibung |
|---|---|
| iter1 = iter2 | Zuweisung |
| DTYP() | Standardkonstruktor |
Ebene 3
Bidirektionaler-Iterator
| Operation | Beschreibung |
|---|---|
| iter-- --iter |
Iterator dekrementieren |
Ebene 4
Random-Access-Iterator
| Operation | Beschreibung |
|---|---|
| iter[n] | Zugriff auf Element iter+n |
| iter += n | Iterator um n erhöhen |
| iter -= n | Iterator um n vermindern |
| iter+n n+iter iter-n |
Iterator um n erhöhen/vermindern |
| iter1 < iter2 iter1 <= iter2 iter1 > iter2 iter1 >= iter2 |
Iteratoren vergleichen |
Wird ein Iterator inkrementiert bzw. dekrementiert sollten die Operationen ++iter bzw. --iter anstelle der Operationen iter++ bzw. iter-- eingesetzt werden. Die ++iter bzw. --iter Operationen arbeitet etwas effizienter.
Um einen Iterator zu initialisieren, d.h. auf den Anfang oder Ende eines Containers/Streams zu setzen, werden die Iterator-Methoden begin() und end() verwendet. Dabei ist zu beachten, dass end() einen Iterator liefert der (CONTAINER_ENDE+1) verweist.
#include <print>
#include <vector>
int main()
{
// std::vector wird gleich noch erklaert
std::vector<int> data {1,2,3,4};
// explizit definiert vector-Iterator
// std::vector<int>::iterator iter;
// Einfacher geht's mit auto
for (auto iter=data.begin(); iter!=data.end(); ++iter)
std::print("{},",*iter);
}
1,2,3,4,
Bei vielen Standard-Containern kann auf die Verwendung von Iteratoren verzichtet werden, um einen Container sequenziell zu durchlaufen. Hierfür eignet sich die range-for-Schleife wesentlich besser. Damit für einen Standard-Container die range-for-Schleife verwendet werden kann, muss der Container die Methoden begin() und end() bereitstellen.
const-Iteratoren
Sollen die Elemente eines Containers per Iterator nicht verändert werden oder ist der Inhalt des Containers selbst konstant, so sind anstelle der Methoden begin() und end() die Methoden cbegin() und cend() zu verwenden.
#include <print>
#include <vector>
int main()
{
// std::vector wird gleich noch erklaert
const std::vector<int> data {1,2,3,4};
// vetor-Inhalt ausgeben
for (auto iter=data.cbegin(); iter<data.cend(); ++iter)
std::print("{},",*iter);
}
1,2,3,4,
Iterator-Funktionen
Mithilfe der Funktion
void std::advance (iiter& iter, distance n);
kann ein Input-Iterator iter um n Positionen vorwärts bewegt werden. Ist iter ein bidirektionaler oder Random-Access Iterator, kann n auch negative Werte annehmen, um den Iterator rückwärts zu bewegen.
#include <print>
#include <vector>
#include <iostream>
int main()
{
// std::vector wird gleich noch erklaert
const std::vector<int> data {1,2,3,4};
// Iterator auf Anfang des Vectors setzen
// und erstes Element ausgeben
auto iter = data.cbegin();
std::println("1. Element: {}",*iter);
// Iterator auf 3. Element
std::advance(iter,2);
std::println("3. Element: {}",*iter);
// 2. Element ausgeben
std::advance(iter,-1);
std::println("2. Element: {}",*iter);
}
1. Element: 1
3. Element: 3
2. Element: 2
Um die Anzahl der Elemente in einem Bereich, der durch zwei Input-Iteratoren definiert ist, zu bestimmen, wird die Funktion
difference_type std::distance (iiter first, iiter last);
verwendet. Und zu guter Letzt können mittels
void std::iter_swap(iter1,iter2)
zwei Elemente, auf die durch Forward-Iteratoren verwiesen wird, vertauscht werden.
#include <print>
#include <vector>
#include <iostream>
int main()
{
// std::vector wird gleich noch erklaert
std::vector<int> data {1,2,3,4};
// Iterator auf Anfang und Ende des Vectors setzen
auto iter1 = data.begin();
auto iter2 = data.end();
// Anzahl der Elemente ausgeben
std::println("Anzahl Elemente: {}",
std::distance(iter1,iter2));
// Erstes und letztes Element tauschen
std::iter_swap(iter1, --iter2);
for (auto element: data)
std::print("{},",element);
}
Anzahl Elemente: 4
4,2,3,1,
Iterator-Adapter
Iterator-Adapter werden hauptsächlich in den später beschriebenen Algorithmen eingesetzt. Trotzdem werden sie an dieser Stelle aufgeführt, um das Thema Iteratoren zusammenzuhalten. Um die Wirkungsweise der Iterator-Adapter zu veranschaulichen, werden wir die Behandlung eines einfachen Algorithmus, den copy-Algorithmus, vorziehen, der in der Header-Datei algorithm deklariert ist.
Der copy-Algorithmus ist wie folgt deklariert:
oiter std::copy(iiter first, iiter last, oiter dest);
Er kopiert die Container-Elemente aus dem Bereich [first...last) in den Ziel-Container ab der Position dest. Hierbei muss der Ziel-Container groß genug sein, um die zu kopierenden Elemente aufnehmen zu können.
#include <print>
#include <vector>
#include <iostream>
int main()
{
// Synonym fuer vector<int> definieren
using cType = std::vector<int>;
// std::vector wird gleich noch erklaert
cType data1 {1,2,3,4,5,6,7,8,9,10};
cType data2 {11,22,33};
// Container ausgeben
std::print("data1: ");
for (auto elem: data1)
std::print("{},",elem);
std::print("\ndata2: ");
for (auto elem: data2)
std::print("{},",elem);
// Ersten beide Elemente von data2 nach data1
// ab der Position 2 kopieren
std::copy(data2.begin(),data2.begin()+2,
data1.begin()+2);
// Container data1 ausgeben
std::print("\ndata1: ");
for (auto elem: data1)
std::print("{},",elem);
}
data1: 1,2,3,4,5,6,7,8,9,10,
data2: 11,22,33,
data1: 1,2,11,22,5,6,7,8,9,10,
Reverse-Iterator
Mithilfe des Reverse-Iterators wird der Inhalt eines Containers in umgekehrter Reihenfolge durchlaufen. Die Definition eines Reverse-Iterators erfolgt bei dessen Initialisierung durch Angabe von rbegin() und rend() anstelle von begin() und end().
Wird z.B. der copy-Algorithmus aus dem vorherigen Beispiel mit Reverse-Iteratoren für die Bereichsangabe aufgerufen, wird der Inhalt des Containers cont2 in umgekehrter Reihenfolge in den Container cont1 kopiert.
#include <print>
#include <vector>
#include <iostream>
int main()
{
// Synonym fuer vector<int> definieren
using cType = std::vector<int>;
// std::vector wird gleich noch erklaert
cType data1 {1,2,3,4,5,6,7,8,9,10};
cType data2 {11,22,33};
// Ersten beide Elemente von data2 nach data1
// ab der Position 2 reverse kopieren
std::copy(data2.rbegin(),data2.rbegin()+2,
data1.begin()+2);
// Container data1 ausgeben
std::print("\ndata1: ");
for (auto elem: data1)
std::print("{},",elem);
}
data1: 1,2,33,22,5,6,7,8,9,10,
Insert-Iteratoren
Während die bisherigen Iteratoren die ursprünglichen Elemente in einem Container überschreiben, fügen Insert-Iteratoren (auch Inserter genannt) Elemente in den Container ein.
Die Standardbibliothek stellt drei verschiedene Inserter zur Verfügung: front_inserter, back_inserter und den allgemeinen inserter.
Und selbstverständlich muss der Container, auf den der Iterator verweist, die Funktionalität bereitstellen, die der Inserter benötigt. So kann z.B. der front_inserter nicht auf einen Vektor angewandt werden, da er, wie wir später noch sehen werden, das Einfügen von Elementen am Containeranfang nicht unterstützt.
front_inserter
Der front_inserter dient zum Einfügen von Elementen am Anfang eines Containers. Der Aufruf der Methode front_inserter(container) erzeugt einen front_inserter-Iterator, der auf den Beginn des Containers container verweist.
#include <print>
#include <deque>
#include <iostream>
int main()
{
// Synonym fuer deque<int> definieren
using cType = std::deque<int>;
// std::vector wird gleich noch erklaert
cType data1 {1,2,3,4,5,6,7,8,9,10};
cType data2 {11,22,33};
// Ersten beide Elemente von data2 nach data1
// ab der Position 2 reverse kopieren
std::copy(data2.begin(),data2.begin()+2,
std::front_inserter(data1));
// Container data1 ausgeben
std::print("\ndata1: ");
for (auto elem: data1)
std::print("{},",elem);
}
data1: 22,11,1,2,3,4,5,6,7,8,9,10,
Der Ursache, dass der Inhalt des Quell-Containers cont2 in umgekehrter Reihenfolge an den Anfang des Ziel-Containers cont1 kopiert wird, liegt darin, dass die Elemente aus cont2 nacheinander ausgelesen werden und jeweils am Anfang des Containers cont1 eingefügt werden.
back_inserter
Der back_inserter(container) dient zum Anfügen von Elementen am Ende Containers container.
#include <print>
#include <deque>
#include <iostream>
int main()
{
// Synonym fuer deque<int> definieren
using cType = std::deque<int>;
// std::vector wird gleich noch erklaert
cType data1 {1,2,3,4,5,6,7,8,9,10};
cType data2 {11,22,33};
// Ersten beide Elemente von data2 nach data1
// ab der Position 2 reverse kopieren
std::copy(data2.begin(),data2.begin()+2,
std::back_inserter(data1));
// Container data1 ausgeben
std::print("\ndata1: ");
for (auto elem: data1)
std::print("{},",elem);
}
data1: 1,2,3,4,5,6,7,8,9,10,11,22,
Allgemeiner inserter
Der allgemeine Inserter inserter(container, pos) kann Elemente an beliebiger Position pos in den Container container einfügen.
#include <print>
#include <deque>
#include <iostream>
int main()
{
// Synonym fuer deque<int> definieren
using cType = std::deque<int>;
// std::vector wird gleich noch erklaert
cType data1 {1,2,3,4,5,6,7,8,9,10};
cType data2 {11,22,33};
// Ersten beide Elemente von data2 nach data1
// ab der Position 2 reverse kopieren
std::copy(data2.begin(),data2.begin()+2,
std::inserter(data1,data1.begin()+2));
// Container data1 ausgeben
std::print("\ndata1: ");
for (auto elem: data1)
std::print("{},",elem);
}
data1: 1,2,11,22,3,4,5,6,7,8,9,10,
In den Beispielen wurden beim Aufruf des copy-Algorithmus die vereinfachten Iterator-Definitionen verwendet. Die vollständige explizite Definition eines Iterators auf ein int-Container sieht folgendermaßen aus:
using cType = deque<int>;
front_insert_iterator<cType> fiter(container);
back_insert_iterator<cType> biter(container);
insert_iterator<cType> iiter(container,pos);
Stream-Iteratoren
Stream-Iteratoren erlauben Algorithmen als Quelle bzw. Ziel einen Stream einzusetzen.
Der ostream_iterator
std::ostream_iterator<DTYP> iter(ostream os, const char* del);
überträgt Daten des Datentyps DTYP in den Ausgabestream ostream, wobei die einzelnen Daten durch den C-String del getrennt werden. Wird ein solcher Iterator als Ziel des copy-Algorithmus verwendet, kann z.B. mit einer einzigen Zeile der komplette Inhalt eines Containers in eine Datei oder auf die Standardausgabe übertragen werden.
#include <print>
#include <vector>
#include <iostream>
#include <iterator>
int main()
{
// Synonym fuer vector<int> definieren
using cType = std::vector<int>;
// std::vector wird gleich noch erklaert
cType data1 {1,2,3,4,5,6,7,8,9,10};
cType data2 {11,22,33};
// Stream-Iterator fuer die Standardausgabe
// definieren; Trennzeichen ist das Komma
// cType::value_type ist der Datentyp der
// im Container abgelegten Daten (hier int).
std::ostream_iterator<cType::value_type> oiter(std::cout,",");
// Daten auf die Standardausgabe 'kopieren'
std::println("data1: ");
std::copy(data1.begin(),data1.end(),oiter);
// Und nun Daten rueckwaerts ausgeben
std::println("\ndata1 rueckwaerts: ");
std::copy(data1.rbegin(),data1.rend(),oiter);
}
data1:
1,2,3,4,5,6,7,8,9,10,
data1 rueckwaerts:
10,9,8,7,6,5,4,3,2,1,
Beachten Sie, wie der Datentyp DTYP in Zeile 17 festgelegt wird. Wie Sie weiter vorne erfahren haben (Gemeinsame Container-Eigenschaften und -Operationen), ist der Bezeichner value_type ein Synonym für den Datentyp der Elemente im Container.
Das Gegenstück zum ostream_iterator ist der istream_iterator
std::istream_iterator<DTYP> iter(istream in);
Er liest Daten des Datentyps DTYP aus dem Eingabestream istream. Wird ein solcher Iterator als Quelle für den copy-Algorithmus verwendet, kann mit einer Zeile eine komplette Datei in einen Container übertragen werden. Das Stream-Ende wird durch den End-of-Stream-Iterator gekennzeichnet.
Wird der istream_iterator als Quelle innerhalb des copy-Algorithmus eingesetzt, ist darauf zu achten, dass der Ziel-Container alle Eingaben aufnehmen kann. Am sichersten ist es, wenn als Ziel ein Inserter eingesetzt wird, so wie im folgenden Beispiel.
#include <print>
#include <deque>
#include <iostream>
#include <fstream>
#include <iterator>
int main()
{
// Container Definition
using cType = std::deque<int>;
cType cont1;
// Datei öffnen
std::ifstream infile("verbrauch.txt");
if (!infile)
{
std::println("Kann Datei nicht oeffnen!");
exit(1);
}
// Iterator für Dateistream
std::istream_iterator<cType::value_type> fileIter(infile);
// End-of-stream Iterator
std::istream_iterator<cType::value_type> eofIter;
// Iterator fuer Standardausgabe
std::ostream_iterator<cType::value_type> oiter(std::cout,", ");
// Komplette Datei in Container cont1 einlesen
std::copy(fileIter, eofIter, std::front_inserter(cont1));
// Container ausgeben
std::println("Eingelesene Daten:");
std::copy(cont1.rbegin(),cont1.rend(),oiter);
// Datei auch wieder schliessen!
infile.close();
}
Eingelesene Daten:
5, 50, 310, 23, 750, 34, 1350, 45, 1600, 20, 2160, 40,
Wenn Sie wollen, können Sie noch eine kleine Übung machen. Versuchen Sie einmal, mit zwei Anweisungen von der Standardeingabe beliebig viele int-Werte einzulesen und diese in einer Datei abzulegen. Die erste Anweisung ist dabei die Anweisung zum Öffnen der Datei. Die Lösung finden Sie im Anhang S: Lösung zu Iteratoren.
Ranges und Views
Ranges
Einzubindende Header-Datei: ranges
Bei fast allen Algorithmen in der Standardbibliothek werden die zu verarbeitenden Daten durch einen Bereich bestimmt. So führt der Algorithmus
std:for_each(data.begin(), data.end(), func);
für jedes Element im Bereich [begin...end) die Funktion func aus.
Um u.a. die Aufrufe der Algorithmen zu vereinfachen, wurden mit C++20 Ranges eingeführt. Damit bestehende Anwendungen nicht umgeschrieben werden müssen, wurden die Algorithmen neu definiert und in einen eigenen Sub-Namensraum std::ranges gelegt.
std::ranges::for_each (data, func[, proj={}]);
D.h anstelle von data.begin() und data.end() wird nur noch der Range data übergeben.
#include <print>
#include <algorithm>
int main()
{
int data[]{ 1,5,-1,3,0,9,-1,3,7 };
// Lambda-Funktion fuer Ausgabe
auto PrintData = [](auto val) {std::print("{}, ", val); };
// Alle Elemente mittels PrintData ausgeben
std::ranges::for_each(data, PrintData);
}
1, 5, -1, 3, 0, 9, -1, 3, 7,
Der dritte optionale Parameter proj ist eine sogenannte Projektion, die entweder die durch den Algorithmus zu verarbeitenden Elemente 'vorverarbeitet' oder die Eigenschaft eines Objekts festlegt, auf die der Algorithmus wirkt.
Nehmen wir an, wir wollen von allen Elementen mithilfe des for_each() Algorithmus die Wurzel ausgeben. Bevor ein Element an die entsprechende Lambda-Funktion PrintSqrt() übergeben wird, soll anstelle eines negativen Werts der Wert 0 an die die Funktion übergeben werden.
#include <print>
#include <algorithm>
#include <cmath>
int main()
{
int data[]{ 1,5,-1,3,0,9,-1,3,7 };
// Lambda-Funktion fuer Ausgabe der Wurzel
auto PrintSqrt = [](auto val)
{
std::print("{:.2f}, ", std::sqrt(val));
};
// Alle Elemente mittels PrintSqrt ausgeben
// Projektion liefert 0 wenn das Element
// im Bereich negativ ist
std::ranges::for_each(data, PrintSqrt,
[](int val)
{
return val<0? 0:val;
});
}
1.00, 2.24, 0.00, 1.73, 0.00, 3.00, 0.00, 1.73, 2.65,
Um einen Algorithmus auf eine bestimmte Eigenschaft eines Objekts anzuwenden, ist als Projektion ein Memberzeiger auf die Eigenschaft anzugeben. Im nachfolgenden Beispiel gibt der for_each() Algorithmus z.B. nur die Personalnamen aus.
#include <print>
#include <algorithm>
struct Pers
{
std::string name;
int id;
};
int main()
{
Pers persData[]
{{"Emil",10},{"Karin",22},{"Else",32},{"Gustav",77}};
// Ausgabefunktion
auto PrintData = [](auto val)
{std::print("{}, ",val);};
// Nur Personalnamen ausgeben
std::ranges::for_each(persData, PrintData, &Pers::name);
}
Emil, Karin, Else, Gustav,
Views
Ein View ist eine Sicht auf einen Range. Er enthält keine Daten, sondern definiert eine Regel, welche Daten wie an einen Algorithmus übergeben werden, d.h., fungiert als eine Art Filter.
Nachfolgend eine Übersicht über die wichtigsten, in der Standardbibliothek definierten, Views, die in der Header-Datei ranges definiert sind und im Namensraum std::views liegen.
| View | Verwendung |
|---|---|
| filter(UPRED) | Sicht enthält nur die Elemente, für die das unäre Predicate UPRED true zurückliefert. |
| transform(FUNC) | Sicht enthält die durch die Funktion FUNC modifizierten Elemente. |
| take(NUM) | Sicht enthält die ersten NUM Elemente. |
| take_while(UPRED) | Sicht enthält die ersten Elemente bis das unäre Predicate UPRED false zurückliefert. |
| drop(NUM) | Sicht enthält alle Elemente ab dem NUM Element. |
| drop_while(UPRED) | Sicht enthält alle Elemente ab dem das unäre Predicate UPRED false zurückliefert. |
| join(VIEW) | Führt zwei Ranges zu einer Sicht zusammen. |
| split(DEL) | Teilt einen Range in Sichten auf mit DEL als Trenner zwischen den Sichten. |
| counted(ST,NUM) | Sicht enthält NUM Elemente ab der Position ST. |
| reverse() | Sicht enthält die Elemente in umgekehrter Reihenfolge. |
Sehen wir uns an, wie ein View auf einen Range angewandt wird. Als Ausgangsbeispiel verwenden wir wiederum den Algorithmus for_each() zur Ausgabe von Daten.
#include <print>
#include <algorithm>
int main()
{
int data[]{ 1,5,-1,3,0,9,-1,3,7 };
// Lambda-Funktion zur Ausgabe der Daten
auto PrintData = [](int val)
{std::print("{}, ", val); };
// Alles ausgeben
std::ranges::for_each(data, PrintData);
}
1, 5, -1, 3, 0, 9, -1, 3, 7,
Soll PrintData nur noch Werte ausgeben, die größer 0 sind, wird ein filter-View eingesetzt. Ein filter-View benötigt laut vorheriger Tabelle ein Predicate, das true zurückliefert, wenn die geforderte Bedingung erfüllt ist.
Um ein View auf einen Range anzuwenden, folgt nach dem Range der Pipe-Operator | und anschließend der View.
#include <print>
#include <algorithm>
#include <ranges>
int main()
{
int data[]{ 1,5,-1,3,0,9,-1,3,7 };
// Lambda-Funktion zur Ausgabe der Daten
auto PrintData = [](int val)
{std::print("{}, ", val); };
// Lambda-Funktion, liefert true wenn val>0
auto Greater0 = [](int val)
{return val>0;};
// Nur Wert groesser 0 ausgeben
std::ranges::for_each(data | std::views::filter(Greater0),
PrintData);
}
1, 5, 3, 9, 3, 7,
Der Operator | in Zeile 17 führt hier keine ODER-Operation aus, sondern übergibt den Range an den View und das 'Ergebnis' des Views wird schließlich an den Algorithmus übergeben.
Außer dass ein View in einem Algorithmus eingesetzt werden kann, kann er ebenfalls bei einer range-for-Schleife eingesetzt werden. Auch hier wird der Range mit dem Pipe-Operator | mit dem View verbunden.
auto greater0 = [](int val)
{ return val > 0; };
for (const auto elem : data | std::views::filter(greater0))
PrintData(elem);
Für einige Views gibt es eine zweite Form, die in der Header-Datei ranges definiert sind. Sie haben alle die Form xxx_view, wobei xxx der Name des Views ist.
// nur Zahlen groesser 0 ausgeben
auto greater0 = [](int val) {return val > 0;};
// View definieren
auto fv = std::ranges::filter_view(
data, greater0);
std::ranges::for_each(fv, PrintData);
Sehen wir uns abschließend die restlichen Views anhand eines Beispiels und dessen Ausgabe an:
#include <print>
#include <ranges>
#include <algorithm>
int main()
{
// Zu verarbeitende Daten
int data[]{ 1,5,-1,3,0,9,-1,3,7 };
int arr[][2]{ {1,2},{3,4},{5,6} };
// Lambda-Funktion zur Ausgabe der Daten
auto PData = [](int val)
{std::print("{}, ", val); };
// Lambda-Funktion zur Ausgabe eines Views
auto PRange = [](const auto& vw)
{
std::print("Sicht: {{");
for (auto elem : vw)
std::print("{}, ", elem);
std::print("}} ");
};
std::println("Ausgangsdaten:");
std::ranges::for_each(data, PData);
std::println("");
std::println("Daten in umgekehrter Reihenfolge:");
std::ranges::for_each(std::views::reverse(data), PData);
std::println("");
std::println("Nur Daten >0:");
auto Greater0 = [](int val) { return val > 0; };
std::ranges::for_each(
data | std::views::filter(Greater0), PData);
std::println("");
std::println("Verschachtelte Ranges zusammenfuehren:");
std::ranges::for_each(std::views::join(arr), PData);
std::println("");
std::println("Mehrere Sichten auf einen Range:");
std::ranges::for_each(data | std::views::split(-1), PRange);
std::println("");
std::println("Erste 3 Daten ausgeben:");
std::ranges::for_each(data | std::views::take(3), PData);
std::println("");
std::println("Erste 2 Daten ueberspringen:");
std::ranges::for_each(data | std::views::drop(2), PData);
std::println("");
std::println("Daten bis zur ersten 0:");
auto Search0 = [](int val) {return val != 0; };
std::ranges::for_each(
data | std::views::take_while(Search0), PData);
std::println("");
std::println("Daten <0 in 0 umwandeln:");
auto Less0 = [](int val)
{ if (val < 0) return 0; else return val; };
std::ranges::for_each(
data | std::views::transform(Less0), PData);
std::println("");
std::println("Ab dem 4. Datum 3 Daten ausgeben:");
std::ranges::for_each(
std::views::counted(std::begin(data) + 3, 3), PData);
std::println("");
std::println("Nur Zahlen >0 mit 2 multiplizieren:");
std::ranges::for_each(data |
std::views::filter(Greater0) |
std::views::transform(
[](int val) { return val * 2; }),
PData);
std::println("");
}
Ausgangsdaten:
1, 5, -1, 3, 0, 9, -1, 3, 7,
Daten in umgekehrter Reihenfolge:
7, 3, -1, 9, 0, 3, -1, 5, 1,
Nur Daten >0:
1, 5, 3, 9, 3, 7,
Verschachtelte Ranges zusammenfuehren:
1, 2, 3, 4, 5, 6,
Mehrere Sichten auf einen Range:
Sicht: {1, 5, } Sicht: {3, 0, 9, } Sicht: {3, 7, }
Erste 3 Daten ausgeben:
1, 5, -1,
Erste 2 Daten ueberspringen:
-1, 3, 0, 9, -1, 3, 7,
Daten bis zur ersten 0:
1, 5, -1, 3,
Daten <0 in 0 umwandeln:
1, 5, 0, 3, 0, 9, 0, 3, 7,
Ab dem 4. Datum 3 Daten ausgeben:
3, 0, 9,
Nur Zahlen >0 mit 2 multiplizieren:
2, 10, 6, 18, 6, 14,
Mehrere Views lassen sich mithilfe des Pipe-Operators | kombinieren. Sehen Sie sich dazu einmal die Zeilen 61...65 an. Auf den Range data wird zuerst ein filter-View angewandt, auf dessen 'Ergebnis' dann ein transform-View und dessen 'Ergebnis' wird schließlich an die Lambda-Funktion PData übergeben. Sie sehen, Ranges und Views sind ein mächtiges Werkzeug, wenn es um die Auswertung von Daten geht.
span
Einzubindende Header-Datei: span
Ein span ist eine Sicht auf kontinuierlich im Speicher liegende Daten. Der Vorteil eines span ist, dass bei Übergabe der Daten z.B. eines Feldes an eine Funktion, automatisch die Größe des Datenfeldes mit übergeben wird.
Definition eines span
Ein span kann u.a. wie folgt definiert werden:
std::span span1<DTYP> {DATA};
std::span span2<DTYP> (PDATA, DSIZE);
DTYP ist der Datentyp, der für die Sicht verwendet wird. Er ist in der Regel mit den im Speicher liegenden Elementen identisch.
Die erste Anweisung erzeugt einen span für das Objekt DATA, wobei DATA ein Feld, ein std::array, ein std::vector (Beschreibungen folgen im nächsten Kapitel) oder ein std::string sein kann.
Die zweite Anweisung erzeugt einen span, beginnend ab der im Zeiger PDATA abgelegten Adresse mit DSIZE Elementen.
#include <print>
#include <span>
void Print(std::span<int> toPrint)
{
for (auto elem : toPrint)
std::print("{},", elem);
std::cout << '\n';
}
int main()
{
// Zu verarbeitende Daten
int data[]{ 1,5,-1,3,0,9,-1,3 };
// span fuer das gesamte Feld data
std::span<int> intSpan1{data};
Print(intSpan1);
// Implizite Konvertierung des Felds in einen span
Print(data);
// span ab der 4. Position mit der Laenge 2-ints
std::span<int> intSpan2(&data[3],2);
Print(intSpan2);
}
1,5,-1,3,0,9,-1,3,
1,5,-1,3,0,9,-1,3,
3,0,
Elementzugriff
Der Zugriff auf die Elemente eines span kann u.a. auf folgende Arten erfolgen.
DTYP& front();
DTYP& back();
Liefert eine Referenz auf das erste bzw. letzte Element.
DTYP& operator[] (size_type INDEX);
Liefert eine Referen auf das INDEX-Element.
Außer diesen direkten Zugriffen kann ebenfalls über Random-Access-Iteratoren auf die Elemente in einem span zugegriffen werden.
Weitere Methoden
span besitzt u.a noch folgende Methoden:
size_type size();
Liefert die Anzahl der Elemente im span.
std::span<DTYP> first(size_t NUM);
std::span<DTYP> last(size_t NUM);
Liefert einen span mit den ersten bzw. letzten NUM Elementen zurück. DTYP muss mit dem Datentyp der Element im span übereinstimmen.
std::span<DTYP> subspan(size_t START, size_t LEN);
Liefert ab der Position START einen weiteren span zurück, der LEN Elemente enthält.
#include <print>
#include <span>
int main()
{
// Zu verarbeitende Daten
int data[]{ 1,5,-1,3,0,9,-1,3 };
// span erzeugen
std::span<int> theSpan{data};
// Anzahl der Elemente
std::println("Anzahl Elemente: {}",theSpan.size());
// Erste 3 Elemente extrahieren
auto span1 = theSpan.first(3);
std::print("span1: ");
for(auto elem: span1)
std::print("{}, ",elem);
// Neue Span mit 3 Elemente beginnend
// ab Position 2 extrahieren
std::print("\nspan2: ");
auto span2 = theSpan.subspan(2,3);
for(auto elem: span2)
std::print("{}, ",elem);
}
Anzahl Elemente: 8
span1: 1, 5, -1,
span2: -1, 3, 0,
Außer einem span gibt es einen mdspan, um die im Speicher liegenden Daten als 2-dimensionales Feld zu interpretieren. Er wird in dieser Einführung nicht weiter betrachtet.
zip-View
Einzubindende Header-Datei: ranges
Ein zip_view erzeugt aus mehreren Sichten eine Sicht, indem er das i-te Element der ersten Sicht mit dem i-ten Element der zweiten usw. verknüpft. Die daraus resultierende Sicht besitzt die Größe der kleinsten zu verknüpfenden Sicht und der Datentyp der Elemente ist vom Typ tuple (siehe Hilfstemplates-tuple). Die Anzahl der Daten in einem tuple entspricht der Anzahl der verknüpften Sichten.
Das nachfolgende Beispiel zeigt exemplarisch die Anwendung eines zip_view auf.
#include <print>
#include <ranges>
#include <string>
using namespace std::string_literals;
int main()
{
// Zu verarbeitende Daten
std::string name[]{ "Emil"s, "Agathe"s, "Franz"s };
unsigned long id[]{ 11,43,23 };
unsigned short income[]{ 2500,4760,3890 };
// zip_view erzeugen
// Fuegt name, id und income zu einer Sicht zusammen
auto zip = std::ranges::zip_view{ name,id,income };
// Alle Elemente des zip_view ausgeben
for (auto [_name, _id, _income]: zip)
std::println("Name:{}, ID:{}, Einkommen:{}",
_name, _id, _income);
}
Name:Emil, ID:11, Einkommen:2500
Name:Agathe, ID:43, Einkommen:4760
Name:Franz, ID:23, Einkommen:3890
Übungen
rngviews_01:
Die Datei aktien.csv enthält 12 Aktienkurse von Unternehmen. Jede Zeile enthält die Daten zu einem Unternehmen in folgender Form:
Adidas;269.70;296.75;01.08.2019
Der erste Eintrag ist der Name des Unternehmens (Adidas), dann folgt der aktuelle Kurs (269.70), der höchste Kurs der Aktie (296.75) sowie das Datum des Höchststandes.
Lesen Sie die Aktiendaten in ein Feld ein und geben sie mithilfe des for_each Algorithmus aus.
Geben Sie alle Aktiendaten von Unternehmen mit dem Anfangsbuchstaben 'D' aus, wobei die Kurswerte in Dollar umzurechnen sind. Verwenden Sie für die Umrechnung einen transform-View und nehmen Sie einen Kurs von 1 EUR = 1.20$ an.
Aktienkurse in EUR:
AG: Adidas, akt 269.70, max 296.75 am 01.08.2019
AG: Bayer, akt 67.34, max 146.20 am 10.04.2015
AG: BMW ST, akt 60.81, max 123.75 am 17.03.2015
AG: Daimler, akt 42.71, max 109.39 am 07.05.1998
AG: Deutsche Bank, akt 6.57, max 108.14 am 14.05.2007
AG: Deutsche Telekom, akt 15.18, max 104.90 am 06.03.2000
AG: Infineon, akt 15.75, max 93.60 am 27.06.2000
AG: Muenchener Rueck, akt 217.90, max 397.73 am 10.11.2000
AG: SAP, akt 108.60, max 125.00 am 03.07.2019
AG: Siemens, akt 90.95, max 133.50 am 04.05.2017
AG: Volkswagen VZ, akt 146.10, max 262.45 am 17.03.2015
AG: Lufthansa, akt 8.50, max 9.20 am 04.09.2019
Aktienkurse der AGs D* in Dollar:
AG: Daimler, akt 51.25, max 131.27 am 07.05.1998
AG: Deutsche Bank, akt 7.88, max 129.77 am 14.05.2007
AG: Deutsche Telekom, akt 18.22, max 125.88 am 06.03.2000