C++20: Die Ranges-Bibliothek

Modernes C++  –  27 Kommentare

Dank der Ranges-Bibliothek in C++20 wird der Umgang mit der Standard Template Library deutlich angenehmer und mächtiger. Ihre Algorithmen sind lazy, agieren direkt auf den Containern und können verknüpft werden. Um es kurz zu machen: Die Bequemlichkeit und Mächtigkeit der Ranges-Bibliothek beruht auf ihren funktionalen Ideen.

Bevor ich auf die Details eingehe, möchte ich ein erstes Beispiel zur Ranges-Bibliothek vorstellen:

// rangesFilterTransform.cpp

#include <iostream>
#include <ranges>
#include <vector>

int main() {

std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

auto results = numbers | std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * 2; });

for (auto v: results) std::cout << v << " "; // 4 8 12

}

Du musst den Ausdruck von links nach rechts lesen. Das Pipe-Symbol steht für die Verknüpfung von Funktionen: Zuerst werden alle Elemente akzeptiert, die gerade sind (std::views::filter([](int n){ return n % 2 == 0; })). Danach wird jedes verbleibende Element verdoppelt (std::views::transform([](int n){ return n * 2; })). Dieses kleine Beispiel zeigt bereits zwei neue Feature der Ranges-Bibliothek: zum einen die Funktionskomposition, und zum anderen agiert diese Funktionskomposition direkt auf dem Container.

Jetzt solltest du für die Details gewappnet sein. Damit sind wir wieder am Startpunkt angelangt: Ranges und Views sind Concepts.

Range

  • std::range: Ein Range ist eine Menge von Elementen, über die iteriert werden kann. Dieser Range verfügt über einen Begin-Iterator und ein Sentinel (Abschlusselement). Selbstverständlich sind die Container der Standard Template Library (STL) Ranges.

Es gibt einige Verfeinerungen von std::range:

  • std::ranges::input_range: steht für einen Range, dessen Iteratoren einem Input-Iterator genügen (damit lässt sich zumindest einmal über den Range iterieren)
  • std::ranges::output_range: steht für einen Range, dessen Iteratoren einem Output-Iterator genügen
  • std::ranges::forward_range: steht für einen Range, dessen Iteratoren einem Forward-Iterator genügen (damit lässt sich mehr als einmal über den Range iterieren)
  • std::ranges::bidirectional_range: steht für einen Range, dessen Iteratoren einem Birdectional-Iterator genügen (damit lässt sich mehr als einmal über den Range vorwärts und rückwärts iterieren)
  • std::ranges::random_access_range: steht für einen Range, dessen Iteratoren einem Random-Access-Iterator genügen (damit lässt sich in konstanter Zeit auf ein beliebiges Element des Ranges mithilfe des Indexoperators [] zugreifen)
  • std::ranges::contiguous_range: steht für einen Range, dessen Iteratoren einem Contiguous-Iterator genügen (die Elemente des Ranges sind kontinuierlich im Speicher angeordnet)

Die Container der STL und der std::string setzen verschiedene Concepts um: Ein Containter, der das Concept std::ranges::contiguous_range unterstützt, unterstützt auch alle vorherigen Concepts in der Tabelle wie std::ranges::random_access_range, std::ranges::bidirectional_range und std::ranges::input_range. Diese Beobachtung gilt natürlich auch für die anderen Ranges der Tabelle.

View

  • Ein View lässt sich auf einen Range anwenden. Dabei wird eine Operation ausgeführt. Views besitzen keine Daten. Konsequenterweise sind seine Copy-, Move- oder Zuweisungsoperationen konstant. Eric Niebler, Autor der ranges-v3-Implementierung, die Grundlage für die Ranges-Bibliothek in C++20 ist, beschreibt Ranges mit folgenden Worten: "Views are composable adaptations of ranges where the adaptation happens lazily as the view is iterated."
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

auto results = numbers | std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * 2; });

In dem Codebeispiel ist numbers die Range, während std::views::filter und std::views::transform für die Views stehen.

Dank der Mächtigkeit der Views erlaubt die Ranges-Bibliothek das Programmieren im funktionalen Stil. Views lassen sich verknüpfen und sind lazy. Ich habe bereits zwei Views vorgestellt. Natürlich enthält C++20 deutlich mehr:

std::all_view, std::views::all // takes all elements

std::ref_view // takes all elements of another view

std::filter_view, std::views::filter // takes the elements which satisfies the predicate

std::transform_view, std::views::transform // transforms each element

std::take_view, std::views::take // takes the first N elements of another view

std::take_while_view, std::views::take_while // takes the elements of another view as long as the predicate returns true

std::drop_view, std::views::drop // skips the first N elements of another view

std::drop_while_view, std::views::drop_while // skips the initial elements of another view until the predicate returns false

std::join_view, std::views::join // joins a view of ranges

std::split_view, std::views::split // splits a view by using a delimiter

std::common_view, std::views::common // converts a view into a std::common_range

std::reverse_view, std::views::reverse // iterates in reverse order

std::basic_istream_view, std::istream_view // applies operator>> on the view

std::elements_view, std::views::elements // creates a view on the N-th element of tuples

std::keys_view, std::views::keys // creates a view on the first element of a pair-like values

std::values_view, std::views::values // creates a view on the second elements of a pair-like values

Im Allgemeinen lässt sich ein View wie std::views::transform mit dem alternativen Namen std::transform_view verwenden. Bei meiner weiteren Vorstellung der Ranges-Bibliothek werde ich die Views anwenden.

Implementierungsstatus

Soweit ich weiß, gibt es zum jetzigen Zeitpunkt (Februar 2020) keine Implementierung der Ranges-Bibliothek. Das ist aber kein Problem, da die bereits erwähnte ranges-v3-Implementierung verfügbar ist. Am einfachsten ist es, den Online-Compiler Wandbox oder den Compiler Explorer mit dem HEAD GCC zu verwenden. Dazu müssen meine Beispiele wie rangesFilterExample.cpp leicht modifiziert werden:

  • Ersetze den Namensraum std::views mit ranges::views.
  • Ersetze die Headerdatei <ranges> mit <ranges/v3/all.hpp>. Mehr Details dazu gibt es in der ranges-v3-Implementierung.
  • Übersetze das Programm mit C++20-Unterstützung: -std=c++2a.
  • Wenn du den Compiler Explorer verwendest, solltest du die trunk-Version der ranges-v3-Implementierung einsetzen. Der folgende Screenshot sollte helfen, die notwendige Option zu finden:

Wende ich die Transformationsschritte auf das Programm rangesFilterTransform.cpp an, erhalte ich das folgende Programm:

// rangesV3FilterTransform.cpp

#include <iostream>
#include <range/v3/all.hpp>
#include <vector>

int main() {

std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

auto results = numbers | ranges::views::filter([](int n){ return n % 2 == 0; })
| ranges::views::transform([](int n){ return n * 2; });

for (auto v: results) std::cout << v << " ";

}

Dank der Wandbox muss ich dieses Mal die Ausgabe nicht vortäuschen.

Wie geht's weiter?

In diesem Artikel ging ich auf die Grundlagen zur Ranges-Bibliothek ein. Dank ihnen kann ich mich in meinem nächsten Artikel auf die Mächtigkeit der Ranges fokussieren. Die Ranges-Bibliothek erweitert C++20 mit zwei neuen Konzepten: Funktionskomposition und Lazy Evaluation. Diese Erweiterung ist genau der Grund dafür, dass die Ranges-Bibliothek zu den großen Vier in C++20 gezählt werden muss. Jede ihrer Komponenten ändert die Art und Weise, wie wir C++ verwenden werden.