Views, évaluation paresseuse et composition
Maîtrisez les views C++20 — des adaptateurs de range légers et paresseux qui se composent avec l'opérateur pipe. Apprenez filter, transform, take, drop, split, join, iota et comment construire de puissants pipelines de données sans allocations intermédiaires.
Que sont les Views ?
Une view est un range léger qui est :
1. Non propriétaire — elle ne possède pas les éléments qu'elle référence
2. Copie et déplacement en O(1) — les views sont peu coûteuses à passer
3. Évaluée paresseusement — les éléments sont calculés à la demande pendant l'itération, pas à l'avance
Les views sont les briques de base des pipelines de range. Au lieu de créer des conteneurs intermédiaires à chaque étape, les views se composent en un seul passage sur les données :
// Traditionnel : crée 2 vectors intermédiaires
auto evens = filter(data, is_even); // allocation
auto squared = transform(evens, sq); // allocation
// Views : zéro allocation intermédiaire
auto result = data | views::filter(is_even) | views::transform(sq);
// Rien n'est calculé encore — result est juste un descripteur de view
for (int x : result) { ... } // calculé élément par élémentLes views vivent dans std::views:: (un alias de namespace pour std::ranges::views::).
filter et transform — Les views fondamentales
views::filter conserve les éléments correspondant à un prédicat. views::transform applique une fonction à chaque élément. Ensemble, ils remplacent la majorité des boucles explicites.
#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 et les views de découpage
views::take(n) produit les n premiers éléments. views::drop(n) saute les n premiers éléments. Ensemble, ils permettent un découpage efficace sans copie.
#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 et les views de traitement de chaînes
views::split divise un range par un délimiteur. views::join aplatit les ranges imbriqués. Ces views sont puissantes pour le traitement de chaînes et de données.
#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 — Générer des ranges
views::iota génère une séquence de valeurs incrémentales. Il peut être borné (iota(0, 10) pour 0..9) ou non borné (iota(0) pour 0, 1, 2, ...). Combiné avec take, il remplace de nombreuses boucles de comptage.
#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
}Construire des pipelines de données complets
La vraie puissance des views émerge lorsque vous composez plusieurs adaptateurs en un seul pipeline. Le pipeline entier s'évalue paresseusement en un seul passage.
#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';
}
}Les views sont non propriétaires — elles référencent le range sous-jacent. Si le range source est détruit, la view devient pendante :
auto make_view() {
std::vector<int> local = {1, 2, 3};
return local | std::views::filter([](int x) { return x > 1; });
// BUG : local est détruit, la view est pendante !
}Attention également aux views sur des temporaires créés en milieu de pipeline :
// FAUX : la chaîne est un temporaire qui est détruit
auto words = std::string("hello world")
| std::views::split(' '); // pendante !Assurez-vous toujours que les données source survivent à toutes les views qui les référencent. Stockez les données dans une variable avec une durée de vie suffisante avant de créer des views dessus.
Les views sont paresseuses — aucun travail ne se produit avant l'itération. Cela signifie :
1. Construire un pipeline est essentiellement gratuit (stocke juste des pointeurs de fonction et des paramètres)
2. Vous pouvez créer des views sur de très grands ranges ou même des ranges infinis
3. Chaque élément traverse le pipeline entier avant que l'élément suivant ne soit traité
4. Les views à court-circuit comme take(n) arrêtent le pipeline entier après n éléments
Pour matérialiser les résultats d'une view dans un conteneur, itérez et collectez :
std::vector<int> results;
for (int x : some_view) results.push_back(x);
// C++23 ajoute std::ranges::to pour une matérialisation plus propre :
auto results = some_view | std::ranges::to<std::vector>();Préférez garder les données sous forme de views le plus longtemps possible — ne matérialisez que lorsque vous avez besoin d'un accès aléatoire, de persistance ou de les passer à des API qui nécessitent des conteneurs.
- Les views sont des adaptateurs de range non propriétaires, copiables en O(1) et évalués paresseusement
views::filteretviews::transformsont les briques de base fondamentales pour les pipelines de donnéesviews::take(n)etviews::drop(n)fournissent un découpage sans copieviews::iotagénère des séquences — bornées ou non bornées — remplaçant les boucles de comptage- L'opérateur pipe
|compose les views en pipelines lisibles de gauche à droite avec zéro allocation intermédiaire - Les views ne doivent pas survivre à leurs données source — assurez-vous toujours que le range sous-jacent a une durée de vie suffisante
Quiz — Testez vos connaissances
(20 XP)1. Quand une view C++20 calcule-t-elle ses éléments ?
2. Quel est le résultat de `std::views::iota(1) | std::views::take(3)` ?
3. Pourquoi est-il dangereux de créer une view sur une variable locale et de la retourner depuis une fonction ?