Zum Inhalt springen

C++20 Ranges: Ein neues Paradigma

Entdecke C++20 Ranges — eine moderne Neugestaltung der STL-Algorithmus-Schnittstelle. Lerne Range-Concepts, Range-basierte Algorithmen, Projektionen und den Pipe-Operator für komponierbare Datentransformationen.

Warum Ranges?

C++20 Ranges sind ein grundlegendes Upgrade des Algorithmus-Modells der STL. Die traditionelle STL erfordert die Übergabe von Iterator-Paaren an jeden Algorithmus, was ausführlich und fehleranfällig ist (man kann versehentlich Iteratoren verschiedener Container übergeben). Ranges lösen dies, indem du den Container selbst übergeben kannst — oder jedes Objekt, das begin() und end() definiert.

Wichtige Verbesserungen gegenüber traditionellen Algorithmen:

1. Container direkt übergeben — std::ranges::sort(vec) statt std::sort(vec.begin(), vec.end())
2. Projektionen — Elemente vor dem Vergleich transformieren, ohne sie zu ändern
3. Concepts — Algorithmen haben klare Kompilierzeit-Anforderungen
4. Komponierbarkeit — Operationen mit dem Pipe-Operator | verketten
5. Lazy Evaluation — Views berechnen Elemente bei Bedarf und vermeiden Zwischenkopien

Range Concepts

C++20 definiert eine Hierarchie von Range-Concepts in :

- std::ranges::range — hat begin() und end()
- std::ranges::sized_range — hat size() in O(1)
- std::ranges::input_range — kann einmal gelesen werden (wie Input-Iteratoren)
- std::ranges::forward_range — mehrfaches Vorwärtsdurchlaufen
- std::ranges::bidirectional_range — vorwärts + rückwärts
- std::ranges::random_access_range — bidirektional + O(1)-Sprünge
- std::ranges::contiguous_range — Random Access + zusammenhängender Speicher
- std::ranges::common_range — begin() und end() geben denselben Typ zurück
- std::ranges::viewable_range — kann in eine View konvertiert werden

Diese Concepts werden verwendet, um Range-basierte Algorithmen einzuschränken und dir klare Fehlermeldungen zu geben, wenn du eine inkompatible Range übergibst.

Range-basierte Algorithmen

Range-basierte Algorithmen befinden sich in std::ranges:: und akzeptieren ganze Container. Sie geben auch reichhaltigere Ergebnistypen zurück als nackte Iteratoren.

range_algorithms.cpp
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

struct Employee {
    std::string name;
    int age;
    double salary;
};

int main() {
    std::vector<int> nums = {5, 3, 1, 4, 1, 5, 9, 2, 6};

    // Pass the container directly — no begin/end needed
    std::ranges::sort(nums);
    // nums: {1, 1, 2, 3, 4, 5, 5, 6, 9}

    // find returns a borrowed iterator (safe with temporary ranges)
    auto it = std::ranges::find(nums, 4);
    if (it != nums.end()) {
        std::cout << "Found: " << *it << '\n';
    }

    // count_if with ranges
    auto even_count = std::ranges::count_if(nums, [](int x) { return x % 2 == 0; });
    std::cout << "Even count: " << even_count << '\n';  // 3

    // min/max on entire range
    auto [min_it, max_it] = std::ranges::minmax_element(nums);
    std::cout << "Min: " << *min_it << ", Max: " << *max_it << '\n';

    // Sorting employees by salary (descending)
    std::vector<Employee> team = {
        {"Alice", 30, 95000},
        {"Bob", 25, 72000},
        {"Charlie", 35, 110000},
    };
    std::ranges::sort(team, std::ranges::greater{}, &Employee::salary);
    // Charlie (110k), Alice (95k), Bob (72k)

    for (const auto& e : team) {
        std::cout << e.name << ": $" << e.salary << '\n';
    }
}

Projektionen — Vor dem Vergleichen transformieren

Projektionen sind ein einzigartiges Merkmal von Range-basierten Algorithmen. Sie ermöglichen es, anzugeben, wie jedes Element für Vergleich oder Verarbeitung "gesehen" wird, ohne das Element selbst zu ändern.

projections.cpp
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <cctype>

int main() {
    // Sort strings case-insensitively using a projection
    std::vector<std::string> words = {"Banana", "apple", "Cherry", "date"};

    std::ranges::sort(words, {}, [](const std::string& s) {
        std::string lower = s;
        std::transform(lower.begin(), lower.end(), lower.begin(),
                       [](char c) { return std::tolower(c); });
        return lower;
    });
    // words: {"apple", "Banana", "Cherry", "date"}
    // Original strings preserved, sorted by lowercase version

    for (const auto& w : words) std::cout << w << ' ';
    std::cout << '\n';

    // Using member pointer as projection
    struct Point { int x, y; };
    std::vector<Point> points = {{3, 1}, {1, 5}, {2, 3}};

    // Sort by x coordinate
    std::ranges::sort(points, {}, &Point::x);
    // points: {{1,5}, {2,3}, {3,1}}

    // Find max by y coordinate
    auto max_y = std::ranges::max_element(points, {}, &Point::y);
    std::cout << "Max y: (" << max_y->x << ", " << max_y->y << ")\n";  // (1, 5)

    // Binary search with projection
    bool found = std::ranges::binary_search(points, 2, {}, &Point::x);
    std::cout << std::boolalpha << "Has x=2: " << found << '\n';  // true
}

Der Pipe-Operator für Komposition

Der Pipe-Operator | ist das Markenzeichen von C++20 Ranges. Er ermöglicht es, Range-Adapter (Views) in einer Pipeline zu verketten, die von links nach rechts gelesen wird, wie eine Unix-Shell-Pipe:

auto result = numbers
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::take(5);

Dies liest sich natürlich: "Nimm Zahlen, behalte die geraden, quadriere sie, nimm die ersten 5." Die Pipeline ist lazy — keine Berechnung findet statt, bis du über result iterierst. Das bedeutet, es werden keine Zwischenvektoren allokiert.

Wir werden Views in der nächsten Lektion ausführlich erkunden.

Stolperfalle

Range-Algorithmen schützen vor Dangling-Iteratoren bei temporären Ranges. Wenn du einen temporären Container übergibst, würde der zurückgegebene Iterator zu einer Dangling-Referenz werden, weil der temporäre Container am Semikolon zerstört wird:

auto it = std::ranges::find(get_vector(), 42);  
// Gibt std::ranges::dangling zurück statt eines Iterators!
// Der Versuch, ihn zu dereferenzieren, wird nicht kompilieren.

Dies ist ein Kompilierzeit-Sicherheitsmerkmal — traditionelles std::find würde stillschweigend einen Dangling-Iterator zurückgeben, was zur Laufzeit zu undefiniertem Verhalten führt. Der Sentinel-Typ std::ranges::dangling macht dies zu einem Kompilierfehler.

Best Practice

Bevorzuge std::ranges::-Algorithmen in neuem Code — sie sind sicherer (keine nicht-passenden Iteratoren, Dangling-Schutz), lesbarer (Container direkt übergeben) und leistungsfähiger (Projektionen, Concepts).

Verwende traditionelle std::-Algorithmen, wenn:
- Dein Code Pre-C++20-Compiler unterstützen muss
- Du eine bestimmte Überladung brauchst, die Ranges nicht bieten
- Du mit Iterator-Paaren aus Legacy-APIs arbeitest

Hinweis: std::ranges::-Algorithmen sind keine Drop-in-Ersetzungen. Einige geben andere Typen zurück (z.B. gibt std::ranges::sort den End-Iterator des sortierten Bereichs zurück). Prüfe die Dokumentation beim Migrieren.

Wichtigste Erkenntnisse
  • C++20 Ranges ermöglichen die direkte Übergabe von Containern an Algorithmen — kein begin()/end()-Boilerplate mehr
  • Projektionen transformieren Elemente für den Vergleich, ohne die Originaldaten zu ändern
  • Range-Concepts (input_range, random_access_range usw.) bieten klare Kompilierzeit-Einschränkungen
  • Der Pipe-Operator | ermöglicht komponierbare Links-nach-rechts-Datenverarbeitungs-Pipelines
  • std::ranges::dangling verhindert Dangling-Iterator-Fehler zur Kompilierzeit
  • Bevorzuge std::ranges::-Algorithmen in neuem C++20-Code für Sicherheit, Lesbarkeit und Leistungsfähigkeit

Quiz — Teste dein Wissen

(20 XP)

1. Was ist eine Projektion im Kontext von C++20-Range-Algorithmen?

2. Was gibt `std::ranges::find` zurück, wenn es mit einem temporären Container aufgerufen wird?

3. Was macht `std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)`?