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.
#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.
#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.
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.
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.
- 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_rangeusw.) bieten klare Kompilierzeit-Einschränkungen - Der Pipe-Operator
|ermöglicht komponierbare Links-nach-rechts-Datenverarbeitungs-Pipelines std::ranges::danglingverhindert 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)`?