Ranges: Verbesserungen mit C++23

Dank C++23 wird die Konstruktion von Containern einfacher. Außerdem hat die Ranges-Bibliothek mehrere neue Views erhalten.

Lesezeit: 6 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen
Von
  • 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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

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.

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.

Bevor ich dir die neuen Views in C++23 zeige, sind hier die bereits in C++20 implementierten:

Jetzt möchte ich dir die neuen Views mit kurzen Codebeispielen vorstellen.

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.

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
}

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';
}

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.

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]
}

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)