Views, Lazy Evaluation & Komposition
Meistere C++20 Views — leichtgewichtige, lazy Range-Adapter, die sich mit dem Pipe-Operator komponieren lassen. Lerne filter, transform, take, drop, split, join, iota und wie man leistungsstarke Datenpipelines ohne Zwischenallokationen baut.
Was sind Views?
Eine View ist eine leichtgewichtige Range, die:
1. Nicht-besitzend ist — sie besitzt nicht die Elemente, auf die sie verweist
2. O(1) Kopieren und Verschieben hat — Views können günstig weitergereicht werden
3. Lazy ausgewertet wird — Elemente werden bei Bedarf während der Iteration berechnet, nicht im Voraus
Views sind die Bausteine von Range-Pipelines. Anstatt bei jedem Schritt Zwischencontainer zu erstellen, komponieren sich Views zu einem einzigen Durchlauf über die Daten:
// Traditionell: erstellt 2 Zwischenvektoren
auto evens = filter(data, is_even); // Allokation
auto squared = transform(evens, sq); // Allokation
// Views: null Zwischenallokationen
auto result = data | views::filter(is_even) | views::transform(sq);
// Noch nichts berechnet -- result ist nur ein View-Deskriptor
for (int x : result) { ... } // Element für Element berechnetViews befinden sich in std::views:: (ein Namespace-Alias für std::ranges::views::).
filter und transform — Die fundamentalen Views
views::filter behält Elemente, die einem Prädikat entsprechen. views::transform wendet eine Funktion auf jedes Element an. Zusammen ersetzen sie die Mehrheit expliziter Schleifen.
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Filter even numbers, then square them
auto even_squares = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
// Nothing has been computed yet — even_squares is a view
for (int x : even_squares) {
std::cout << x << ' '; // 4 16 36 64 100
}
std::cout << '\n';
// Views work with any range — including strings
std::string text = "Hello, World!";
auto uppercase = text
| std::views::filter([](char c) { return std::isalpha(c); })
| std::views::transform([](char c) { return std::toupper(c); });
for (char c : uppercase) {
std::cout << c; // HELLOWORLD
}
std::cout << '\n';
// Collect view results into a vector (C++23: std::ranges::to)
std::vector<int> collected;
for (int x : even_squares) {
collected.push_back(x);
}
// C++23: auto collected = even_squares | std::ranges::to<std::vector>();
}take, drop und Slicing-Views
views::take(n) liefert die ersten n Elemente. views::drop(n) überspringt die ersten n Elemente. Zusammen ermöglichen sie effizientes Slicing ohne Kopieren.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// take — first N elements
for (int x : data | std::views::take(3)) {
std::cout << x << ' '; // 1 2 3
}
std::cout << '\n';
// drop — skip first N elements
for (int x : data | std::views::drop(7)) {
std::cout << x << ' '; // 8 9 10
}
std::cout << '\n';
// Combine: elements 3-7 (0-indexed: drop 3, take 4)
for (int x : data | std::views::drop(3) | std::views::take(4)) {
std::cout << x << ' '; // 4 5 6 7
}
std::cout << '\n';
// take_while / drop_while — predicate-based
std::vector<int> sorted_data = {1, 3, 5, 7, 2, 4, 6};
auto ascending_prefix = sorted_data | std::views::take_while(
[prev = 0](int x) mutable {
bool ok = x > prev;
prev = x;
return ok;
});
for (int x : ascending_prefix) {
std::cout << x << ' '; // 1 3 5 7
}
std::cout << '\n';
// Pagination example: page 2, page_size = 3
int page = 2, page_size = 3;
auto page_view = data
| std::views::drop((page - 1) * page_size)
| std::views::take(page_size);
for (int x : page_view) {
std::cout << x << ' '; // 4 5 6
}
std::cout << '\n';
}split, join und String-Verarbeitungs-Views
views::split teilt eine Range durch ein Trennzeichen. views::join flacht verschachtelte Ranges ab. Diese sind leistungsstark für String- und Datenverarbeitung.
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include <iostream>
int main() {
// split — divide a string by delimiter
std::string csv = "Alice,30,Engineer,95000";
for (auto field : csv | std::views::split(',')) {
// Each 'field' is a subrange — convert to string_view
std::string_view sv(field.begin(), field.end());
std::cout << '[' << sv << "] ";
}
std::cout << '\n'; // [Alice] [30] [Engineer] [95000]
// join — flatten nested ranges
std::vector<std::vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int x : matrix | std::views::join) {
std::cout << x << ' '; // 1 2 3 4 5 6 7 8 9
}
std::cout << '\n';
// Combine: split lines, then process each line
std::string multiline = "hello world\nfoo bar\nbaz qux";
int line_num = 1;
for (auto line : multiline | std::views::split('\n')) {
std::string_view line_sv(line.begin(), line.end());
std::cout << line_num++ << ": " << line_sv << '\n';
}
// Enumerate (C++23)
// for (auto [i, val] : data | std::views::enumerate) { ... }
}views::iota — Ranges generieren
views::iota generiert eine Folge aufsteigender Werte. Es kann begrenzt (iota(0, 10) für 0..9) oder unbegrenzt (iota(0) für 0, 1, 2, ...) sein. Kombiniert mit take ersetzt es viele Zählschleifen.
#include <ranges>
#include <iostream>
#include <vector>
#include <numeric>
int main() {
// Bounded iota — replaces for(int i = 0; i < 10; ++i)
for (int x : std::views::iota(0, 10)) {
std::cout << x << ' '; // 0 1 2 3 4 5 6 7 8 9
}
std::cout << '\n';
// Unbounded iota + take — first 5 squares
auto first_5_squares = std::views::iota(1)
| std::views::transform([](int x) { return x * x; })
| std::views::take(5);
for (int x : first_5_squares) {
std::cout << x << ' '; // 1 4 9 16 25
}
std::cout << '\n';
// Generate Fibonacci-like sequence with views
// (iota as index generator)
auto indices = std::views::iota(0, 20);
auto even_indices = indices | std::views::filter([](int i) { return i % 2 == 0; });
for (int i : even_indices | std::views::take(5)) {
std::cout << i << ' '; // 0 2 4 6 8
}
std::cout << '\n';
// Use iota to create a vector of sequential values
auto range_vec = std::views::iota(1, 11); // 1 to 10
int sum = 0;
for (int x : range_vec) sum += x;
std::cout << "Sum 1..10: " << sum << '\n'; // 55
}Vollständige Datenpipelines bauen
Die wahre Stärke von Views zeigt sich, wenn du mehrere Adapter zu einer einzigen Pipeline komponierst. Die gesamte Pipeline wertet lazy in einem einzigen Durchlauf aus.
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
struct LogEntry {
std::string level; // "INFO", "WARN", "ERROR"
std::string message;
int timestamp;
};
int main() {
std::vector<LogEntry> logs = {
{"INFO", "Server started", 1000},
{"ERROR", "Connection refused", 1001},
{"INFO", "Request received", 1002},
{"WARN", "Slow query detected", 1003},
{"ERROR", "Timeout expired", 1004},
{"INFO", "Request completed", 1005},
{"ERROR", "Disk full", 1006},
{"WARN", "Memory usage high", 1007},
};
// Pipeline: get recent error messages, formatted
auto recent_errors = logs
| std::views::filter([](const LogEntry& e) {
return e.level == "ERROR";
})
| std::views::drop(1) // skip first error
| std::views::transform([](const LogEntry& e) {
return "[" + std::to_string(e.timestamp) + "] " + e.message;
})
| std::views::take(5); // at most 5 results
std::cout << "Recent errors (after first):\n";
for (const auto& msg : recent_errors) {
std::cout << " " << msg << '\n';
}
// [1004] Timeout expired
// [1006] Disk full
// Reverse view — iterate backward without copying
auto reversed = logs | std::views::reverse | std::views::take(3);
std::cout << "\nLast 3 entries (reversed):\n";
for (const auto& e : reversed) {
std::cout << " [" << e.level << "] " << e.message << '\n';
}
}Views sind nicht-besitzend — sie referenzieren die zugrunde liegende Range. Wenn die Quell-Range zerstört wird, wird die View zu einer Dangling-Referenz:
auto make_view() {
std::vector<int> local = {1, 2, 3};
return local | std::views::filter([](int x) { return x > 1; });
// BUG: local wird zerstört, View dangled!
}Achte auch auf Views über temporäre Objekte, die mitten in der Pipeline erstellt werden:
// FALSCH: Der String ist ein temporäres Objekt, das zerstört wird
auto words = std::string("hello world")
| std::views::split(' '); // dangling!Stelle immer sicher, dass die Quelldaten alle Views überleben, die auf sie verweisen. Speichere die Daten in einer Variable mit ausreichender Lebensdauer, bevor du Views darüber erstellst.
Views sind lazy — keine Arbeit geschieht, bis iteriert wird. Das bedeutet:
1. Eine Pipeline aufzubauen ist im Wesentlichen kostenlos (speichert nur Funktionszeiger und Parameter)
2. Du kannst Views über sehr große oder sogar unendliche Ranges erstellen
3. Jedes Element durchläuft die gesamte Pipeline, bevor das nächste Element verarbeitet wird
4. Kurzschluss-Views wie take(n) stoppen die gesamte Pipeline nach n Elementen
Um View-Ergebnisse in einen Container zu materialisieren, iteriere und sammle:
std::vector<int> results;
for (int x : some_view) results.push_back(x);
// C++23 fügt std::ranges::to für sauberere Materialisierung hinzu:
auto results = some_view | std::ranges::to<std::vector>();Bevorzuge es, Daten so lange wie möglich als Views zu halten — materialisiere nur, wenn du Direktzugriff, Persistenz brauchst oder APIs bedienen musst, die Container erfordern.
- Views sind nicht-besitzende, O(1)-kopierbare, lazy ausgewertete Range-Adapter
views::filterundviews::transformsind die fundamentalen Bausteine für Datenpipelinesviews::take(n)undviews::drop(n)bieten Slicing ohne Kopierenviews::iotageneriert Folgen — begrenzt oder unbegrenzt — und ersetzt Zählschleifen- Der Pipe-Operator
|komponiert Views zu lesbaren Links-nach-rechts-Pipelines mit null Zwischenallokationen - Views dürfen ihre Quelldaten nicht überleben — stelle immer sicher, dass die zugrunde liegende Range eine ausreichende Lebensdauer hat
Quiz — Teste dein Wissen
(20 XP)1. Wann berechnet eine C++20 View ihre Elemente?
2. Was ist das Ergebnis von `std::views::iota(1) | std::views::take(3)`?
3. Warum ist es gefährlich, eine View über eine lokale Variable zu erstellen und sie von einer Funktion zurückzugeben?