Ranges: Verbesserungen mit C++23
Dank C++23 wird die Konstruktion von Containern einfacher. AuĂźerdem hat die Ranges-Bibliothek mehrere neue Views erhalten.
- Rainer Grimm
C++23 ist kein so bedeutender Standard wie C++11 oder C++20. Er steht eher in der Tradition von C++17. Das liegt vor allem an COVID-19, weil die jährlichen vier persönlichen Treffen online stattfanden. Im Grunde genommen ist die Ranges-Bibliothek die Ausnahme von dieser Regel. Die Ranges werden ein paar entscheidende Ergänzungen erhalten.
Wer mehr darĂĽber wissen willt, wie es mit C++23 weitergeht (bevor ich darĂĽber schreibe), kann sich cppreference.com/compiler_support anschauen. Noch besser ist es, den hervorragenden Artikel von Steve Downey zu lesen: C++23 Status Report.
Konstruktion von Containern
Einen Container aus einem Range zu konstruieren, war eine komplizierte Aufgabe. Die folgende Funktion range
simuliert die range
-Funktion von Python2. Diese ist eager, ebenso wie ihr range
-Pendant. Eager heiĂźt, dass sie ihre Elemente sofort und nicht erst auf Anfrage erzeugt. AuĂźerdem gibt Pythons range
-Funktion eine Liste zurĂĽck, meine aber einen std::vector
.
// range.cpp
#include <iostream>
#include <range/v3/all.hpp>
#include <vector>
std::vector<int> range(int begin, int end, int stepsize = 1) {
std::vector<int> result{};
if (begin < end) { // (5)
auto boundary = [end](int i){ return i < end; };
for (int i: ranges::views::iota(begin)
| ranges::views::stride(stepsize)
| ranges::views::take_while(boundary)) {
result.push_back(i);
}
}
else { // (6)
begin++;
end++;
stepsize *= -1;
auto boundary = [begin](int i){ return i < begin; };
for (int i: ranges::views::iota(end)
| ranges::views::take_while(boundary)
| ranges::views::reverse
| ranges::views::stride(stepsize)) {
result.push_back(i);
}
}
return result;
}
int main() {
std::cout << std::endl;
// range(1, 50) // (1)
auto res = range(1, 50);
for (auto i: res) std::cout << i << " ";
std::cout << "\n\n";
// range(1, 50, 5) // (2)
res = range(1, 50, 5);
for (auto i: res) std::cout << i << " ";
std::cout << "\n\n";
// range(50, 10, -1) // (3)
res = range(50, 10, -1);
for (auto i: res) std::cout << i << " ";
std::cout << "\n\n";
// range(50, 10, -5) // (4)
res = range(50, 10, -5);
for (auto i: res) std::cout << i << " ";
std::cout << "\n\n";
}
Dank des Studiums der Ausgabe, sollten (1) bis (4) ziemlich einfach zu lesen sein.
Die ersten beiden Argumente des range
-Aufrufs stehen für den Anfang und das Ende der erzeugten Zahlen. Der Anfang wird mit einbezogen, das Ende jedoch nicht. Die Schrittweite als dritter Parameter ist standardmäßig 1. Wenn das Intervall [begin, end]
absteigt, sollte die Schrittweite negativ sein. Ansonsten erhält man eine leere Liste oder einen leeren std::vector<int>
.
In meiner Bereichsimplementierung schummle ich ein wenig. Ich verwende die Funktion ranges::views::stride
, die nicht Teil von C++20 ist. stride(n)
gibt das n-te Element des angegebenen Bereichs zurĂĽck. Ich nehme an, dass std::views::stride
Teil von C++23 sein wird. Folglich habe ich in meinem Beispiel die Ranges v3-Implementierung verwendet, aber nicht die C++20-Implementierung der Ranges-Bibliothek.
Die if-
Bedingung (begin < end
) der ranges-Funktion in (1) sollte recht einfach zu lesen sein: Erstelle alle Zahlen, die mit begin beginnen (ranges::views::iota(begin)
), nehme jedes n-te Element (ranges::views::stride(stepsize)
) und mache das so lange, wie die Randbedingung gilt (ranges::views::take_while(boundary
). Zum Schluss werden alle Zahlen in den std::vector<int>
geschoben. Im else
-Fall (Zeile 2) verwende ich einen kleinen Trick. Ich erstelle die Zahlen [end++, begin++[,
nehme sie, bis die Randbedingung erfĂĽllt ist, drehe sie um (ranges::views::reverse
) und nehme jedes n-te Element.
Gehen wir nun davon aus, dass std::views::stride
Teil von C++23 ist. Dank std::ranges::to
ist es ziemlich einfach, einen Container zu konstruieren. Hier ist die C++23-basierte Implementierung der vorherigen range
-Funktion:
std::vector<int> range(int begin, int end, int stepsize = 1) {
std::vector<int> result{};
if (begin < end) {
auto boundary = [end](int i){ return i < end; };
result = std::ranges::views::iota(begin)
| std::views::stride(stepsize)
| std::views::take_while(boundary)
| std::ranges::to<std::vector>();
}
else {
begin++;
end++;
stepsize *= -1;
auto boundary = [begin](int i){ return i < begin; };
result = std::ranges::views::iota(end)
| std::views::take_while(boundary)
| std::views::reverse
| std::views::stride(stepsize)
| std::ranges::to<std::vector>();
}
return result;
}
Im Wesentlichen habe ich die push_back
-Operation fĂĽr den std::vector
durch den neuen Aufruf std::ranges::to<std::vector>
ersetzt und bin so zwei Codezeilen losgeworden. Bisher unterstĂĽtzt noch kein Compiler diese neue praktische Funktion zum Erstellen eines Containers. Daher habe ich die neue Implementierung der range
-Funktion auf der Grundlage meiner Interpretation der Spezifikation erstellt. Sollte ein Fehler enthalten sein, werde ich ihn beheben.
Bestehende Views in C++20
Bevor ich dir die neuen Views in C++23 zeige, sind hier die bereits in C++20 implementierten:
Neue Views in C++23
Jetzt möchte ich dir die neuen Views mit kurzen Codebeispielen vorstellen.
std::ranges::zip_transform_view
und std::views::zip_transform
erzeugt einen View, die aus Tupel besteht, indem eine Transformationsfunktion angewendet wird.
Hier ist ein schönes Beispiel von cppreferene.com/zip_transform_view:
#include <list>
#include <array>
#include <ranges>
#include <vector>
#include <iostream>
void print(auto const rem, auto const& r) {
for (std::cout << rem; auto const& e : r)
std::cout << e << ' ';
std::cout << '\n';
}
int main() {
auto v1 = std::vector<float>{1, 2, 3};
auto v2 = std::list<short>{1, 2, 3, 4};
auto v3 = std::to_array({1, 2, 3, 4, 5});
auto add = [](auto a, auto b, auto c) { return a + b + c; };
auto sum = std::views::zip_transform(add, v1, v2, v3);
print("v1: ", v1); // 1 2 3
print("v2: ", v2); // 1 2 3 4
print("v3: ", v3); // 1 2 3 4 5
print("sum: ", sum); // 3 6 9
}
Ich habe die Ausgabe direkt in den Sourcecode eingefĂĽgt.
std::ranges::adjacent_view, std::views::adjacent, std::ranges::adjacent_transform_view
und std::views::adjacent_transform
erzeugt einen View, der aus Tupel von benachbarte Elemente besteht. Zusätzlich lässt sich eine Transformationsfunktion einsetzen.
Diese Beispiele stammen direkt aus dem Proposal P2321R2:
vector v = {1, 2, 3, 4};
for (auto i : v | views::adjacent<2>) {
// prints: (1, 2) (2, 3) (3, 4):
cout << '(' << i.first << ', ' << i.second << ") ";
}
for (auto i : v
| views::adjacent_transform<2>(std::multiplies())) {
cout << i << ' '; // prints: 2 6 12
}
std::ranges::join_with_view
und std::views::join_with
erzeugt einen View, der aus der Verflachung des Eingaberanges besteht. Setzt einen Begrenzer zwischen die Elemente.
cppreference.com/join_with_view stellt ein schönes Beispiel vor, in dem ein Leerzeichen das Begrenzungselement ist.
#include <iostream>
#include <ranges>
#include <vector>
#include <string_view>
int main() {
using namespace std::literals;
std::vector v{"This"sv, "is"sv, "a"sv, "test."sv};
auto joined = v | std::views::join_with(' ');
for (auto c : joined) std::cout << c;
std::cout << '\n';
}
std::views::chunk
und std::views::chunk_by
erzeugt einen View, indem sie einen Range R in nicht überlappende Stücken der Größe N unterteilen. Zusätzlich lässt sich auch ein Prädikat verwenden.
Die Codeschnipsel stammen aus dem Proposal P2442R1 und dem Proposal P2443R1.
std::vector v = {1, 2, 3, 4, 5};
fmt::print("{}\n", v | std::views::chunk(2));
// [[1, 2], [3, 4], [5]]
fmt::print("{}\n", v | std::views::slide(2));
// [[1, 2], [2, 3], [3, 4], [4, 5]]
std::vector v = {1, 2, 2, 3, 0, 4, 5, 2};
fmt::print("{}\n", v
| std::views::chunk_by(ranges::less_equal{}));
Beide Codeschnipsel verwenden die Prototyp-Bibliothek fmt fĂĽr die Formatbibliothek in C++20. fmt besitzt eine Funktion fmt::print
, die wohl als std::print
Bestandteil von C++23 werden wird.
std::views::slide
erzeugt einen View aus N-Tupel, indem es eine View und eine Zahl N annimmt.
Das Beispiel stammt ebenfalls aus dem Proposal P2443R1
vector v = {1, 2, 3, 4};
for (auto i : v | views::slide(2)) {
cout << '[' << i[0] << ', ' << i[1] << "] ";
// [1, 2] [2, 3] [3, 4]
}
Wie geht's weiter?
Letzte Woche habe ich eine Umfrage durchgeführt und gefragt: "Welches Mentoring-Programm soll ich als Nächstes anbieten?" Ehrlich gesagt, hat mich das Ergebnis der Umfrage sehr überrascht. Ich habe von 2004 bis 2008 viele Vorträge zu Design Patterns gehalten und bin davon ausgegangen, dass der Großteil meiner Leser sie bereits kennt. Daher war meine falsche Annahme, dass die Themen "C++20" oder "Clean Code with C++" die Umfrage gewinnen würden. Folglich habe ich auch den Plan für meine kommenden Beiträge geändert. Mein nächstes großes Thema wird "Design Pattern und Architectural Pattern in C++" sein.
Wenn ich dieses groĂźe Thema abgeschlossen habe, werde ich zu C++20 und C++23 zurĂĽckkehren. (rme)