zurück zum Artikel

Ranges: Verbesserungen mit C++23

Rainer Grimm

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.

Modernes C++ – Rainer Grimm
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 [1] anschauen. Noch besser ist es, den hervorragenden Artikel von Steve Downey zu lesen: C++23 Status Report [2].

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.

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 [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.

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
}

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

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.

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

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