Verbesserte Iteratoren mit Ranges
Es gibt noch mehr Gründe, die Ranges-Bibliothek der klassischen STL vorzuziehen. Sie bieten einheitliche Lookup-Regeln und zusätzliche Sicherheitsgarantien.
- Rainer Grimm
Es gibt noch mehr Gründe, die Ranges-Bibliothek der klassischen Standard Template Library vorzuziehen. Die Ranges-Iteratoren unterstützen einheitliche Lookup-Regeln und bieten zusätzliche Sicherheitsgarantien.
Vereinheitlichte Lookup-Regeln
Angenommen, du möchtest eine generische Funktion implementieren, die begin
auf einem Container aufruft. Die Frage ist, ob die Funktion, die begin
auf dem Container aufruft, eine freie begin
-Funktion oder eine Memberfunktion begin
annehmen soll?
// begin.cpp
#include <cstddef>
#include <iostream>
#include <ranges>
struct ContainerFree { // (1)
ContainerFree(std::size_t len): len_(len), data_(new int[len]){}
size_t len_;
int* data_;
};
int* begin(const ContainerFree& conFree) { // (2)
return conFree.data_;
}
struct ContainerMember { // (3)
ContainerMember(std::size_t len): len_(len), data_(new int[len]){}
int* begin() const { // (4)
return data_;
}
size_t len_;
int* data_;
};
void callBeginFree(const auto& cont) { // (5)
begin(cont);
}
void callBeginMember(const auto& cont) { // (6)
cont.begin();
}
int main() {
const ContainerFree contFree(2020);
const ContainerMember contMemb(2023);
callBeginFree(contFree);
callBeginMember(contMemb);
callBeginFree(contMemb); // (7)
callBeginMember(contFree); // (8)
}
ContainerFree
(Zeile 1) besitzt eine freie Funktion begin
(Zeile 2), und ContainerMember
(Zeile 3) hat eine Memberfunktion begin
(Zeile 4). Dementsprechend kann contFree
die generische Funktion callBeginFree
mit dem freien Funktionsaufruf begin(cont)
(Zeile 5) und contMemb
kann die generische Funktion callBeginMember
mit dem Member-Funktionsaufruf cont.begin
(Zeile 6) verwenden. Wenn ich callBeginFree
und callBeginMember
mit den ungeeigneten Containern in Zeile (7) und (8) aufrufe, schlägt die Kompilierung fehl.
Dieses Problem lässt sich lösen, indem ich zwei verschiedene begin
-Implementierungen bereitstelle: klassisch und auf Range basierend.
// beginSolved.cpp
#include <cstddef>
#include <iostream>
#include <ranges>
struct ContainerFree {
ContainerFree(std::size_t len): len_(len), data_(new int[len]){}
size_t len_;
int* data_;
};
int* begin(const ContainerFree& conFree) {
return conFree.data_;
}
struct ContainerMember {
ContainerMember(std::size_t len): len_(len), data_(new int[len]){}
int* begin() const {
return data_;
}
size_t len_;
int* data_;
};
void callBeginClassical(const auto& cont) {
using std::begin; // (1)
begin(cont);
}
void callBeginRanges(const auto& cont) {
std::ranges::begin(cont); // (2)
}
int main() {
const ContainerFree contFree(2020);
const ContainerMember contMemb(2023);
callBeginClassical(contFree);
callBeginRanges(contMemb);
callBeginClassical(contMemb);
callBeginRanges(contFree);
}
Der klassische Weg, dieses Problem zu lösen, besteht darin, std::begin
mit einer sogenannten using-Deklaration (Zeile 1) in den Geltungsbereich zu bringen. Dank ranges
kannst aber auch direkt std::ranges::begin
verwenden (Zeile 2). std::ranges::begin
berĂĽcksichtigt beide Implementierungen von begin
: die freie Version und die Memberfunktion.
Abschließend möchte ich noch etwas zur Sicherheit schreiben.
Sicherheit
Beginnen wir mit Iteratoren.
Iteratoren
Die Ranges-Bibliothek bietet die erwarteten Operationen fĂĽr den Zugriff auf den Bereich.
Wenn du diese Operationen für den Zugriff auf den zugrunde liegenden Bereich verwendest, gibt es einen großen Unterschied. Die Kompilierung schlägt fehl, wenn du die Variante von std::ranges
verwendest und das Argument ein rvalue ist. Im Gegensatz dazu stellt die Verwendung der klassischen std
-Variante undefiniertes Verhalten dar.
// rangesAccess.cpp
#include <iterator>
#include <ranges>
#include <vector>
int main() {
auto beginIt1 = std::begin(std::vector<int>{1, 2, 3});
auto beginIt2 = std::ranges::begin(std::vector<int>{1, 2, 3});
}
std::ranges::begin
bietet nur Überladungen für lvalues an. Der temporäre Vektor std::vector{1, 2, 3}
ist aber ein rvalue. Folglich schlägt die Kompilierung des Programms fehl.
Die AbkĂĽrzungen lvalue und rvalue stehen fĂĽr locatable value und readable value.
- lvalue (locatable value): Ein locatable Value (auffindbarer Wert) ist ein Objekt, das einen Ort im Speicher hat und dessen Adresse du daher bestimmen kannst. Ein lvalue besitzt eine Identität.
- rvalue (readable value): Ein rvalue ist ein Wert, von dem du nur lesen kannst. Er stellt kein Objekt im Speicher dar, und du kannst seine Adresse nicht bestimmen.
Ich muss zugeben, dass meine kurzen Erklärungen zu lvalues und rvalues eine Vereinfachung darstellen. Wenn du mehr Details über Wertkategorien wissen willst, lies den Beitrag "Value Categories".
Übrigens: Nicht nur Iteratoren, sondern auch Views bieten diese zusätzlichen Sicherheitsgarantien.
Views
Views besitzen keine Daten. Deshalb verlängern Views die Lebensdauer ihrer Daten nicht und können nur auf lvalues verwendet werden. Die Kompilierung schlägt fehl, wenn du eine View auf einen temporären Range erstellst.
// temporaryRange.cpp
#include <initializer_list>
#include <ranges>
int main() {
const auto numbers = {1, 2, 3, 4, 5};
auto firstThree = numbers | std::views::drop(3); // (1)
// auto firstThree = {1, 2, 3, 4, 5} | std::views::drop(3); // (2)
std::ranges::drop_view firstFour{numbers, 4}; // (3)
// std::ranges::drop_view firstFour{{1, 2, 3, 4, 5}, 4}; // (4)
}
Wenn die Zeilen 1 und 3 mit dem lvalues numbers
verwendet werden, ist alles in Ordnung. Im Gegensatz dazu fĂĽhrt die Verwendung der auskommentierten Zeilen 2 und 4 fĂĽr den rvalue std::initializer_list<int> {1, 2, 3, 4, 5}
zu einer wortreichen Beschwerde des GCC-Compilers:
Wie geht es jetzt weiter?
In meinem nächsten Artikel werfe ich einen ersten Blick in die Zukunft: auf C++23. Vor allem die Ranges-Bibliothek wird viele Verbesserungen erhalten. So gibt es mit std::ranges::to
eine bequeme Möglichkeit, Container aus Ranges zu konstruieren. Außerdem werden wir fast zwanzig neue, zusätzliche Algorithmen bekommen. Hier sind einige von ihnen: std::views::chunk_by
, std::views::slide
, std::views::join_with
, std::views::zip_transform
und std::views::adjacent_transform
.
(map)