Ranges: Verbesserungen mit C++23
Dank C++23 wird die Konstruktion von Containern einfacher. AuĂerdem hat die Ranges-Bibliothek mehrere neue Views erhalten.
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 [1] anschauen. Noch besser ist es, den hervorragenden Artikel von Steve Downey zu lesen: C++23 Status Report [2].
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 [3] 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 [4]:
#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 [5]:
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 [6] 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 [7]und dem Proposal P2443R1 [8].
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 [9] 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 [10]
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 [11])
URL dieses Artikels:
https://www.heise.de/-7144768
Links in diesem Artikel:
[1] https://en.cppreference.com/w/cpp/compiler_support
[2] https://github.com/steve-downey/papers/blob/master/wg21-status.org
[3] https://github.com/ericniebler/range-v3
[4] https://en.cppreference.com/w/cpp/ranges/zip_transform_view
[5] https://wg21.link/P2321R2
[6] https://en.cppreference.com/w/cpp/ranges/join_with_view
[7] https://wg21.link/P2442R1
[8] https://wg21.link/P2443R1
[9] https://github.com/fmtlib/fmt
[10] https://wg21.link/P2443R1
[11] mailto:rme@ix.de
Copyright © 2022 Heise Medien