Aller au contenu

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ément

Les 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.

filter_transform.cpp
#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.

take_drop.cpp
#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.

split_join.cpp
#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.

views_iota.cpp
#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.

data_pipeline.cpp
#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';
    }
}
Piège

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.

Bonne pratique

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.

Points clés
  • Les views sont des adaptateurs de range non propriétaires, copiables en O(1) et évalués paresseusement
  • views::filter et views::transform sont les briques de base fondamentales pour les pipelines de données
  • views::take(n) et views::drop(n) fournissent un découpage sans copie
  • views::iota gé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 ?