Viste, valutazione lazy e composizione
Padroneggia le viste C++20 — adattatori di range leggeri e lazy che si compongono con l'operatore pipe. Scopri filter, transform, take, drop, split, join, iota e come costruire potenti pipeline di dati senza allocazioni intermedie.
Cosa sono le viste?
Una vista è un range leggero che è:
1. Non proprietario — non possiede gli elementi a cui fa riferimento
2. Copia e spostamento O(1) — le viste sono economiche da passare
3. Valutate in modo lazy — gli elementi vengono calcolati su richiesta durante l'iterazione, non in anticipo
Le viste sono i mattoni delle pipeline di range. Invece di creare contenitori intermedi ad ogni passaggio, le viste si compongono in un singolo passaggio sui dati:
// Tradizionale: crea 2 vector intermedi
auto evens = filter(data, is_even); // allocazione
auto squared = transform(evens, sq); // allocazione
// Viste: zero allocazioni intermedie
auto result = data | views::filter(is_even) | views::transform(sq);
// Nulla è ancora calcolato — result è solo un descrittore di vista
for (int x : result) { ... } // calcolato elemento per elementoLe viste risiedono in std::views:: (un alias del namespace std::ranges::views::).
filter e transform — Le viste fondamentali
views::filter mantiene gli elementi che corrispondono a un predicato. views::transform applica una funzione a ogni elemento. Insieme, sostituiscono la maggior parte dei cicli espliciti.
#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 e viste di slicing
views::take(n) produce i primi n elementi. views::drop(n) salta i primi n elementi. Insieme consentono lo slicing efficiente senza copiare.
#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 e viste per l'elaborazione di stringhe
views::split divide un range in base a un delimitatore. views::join appiattisce range annidati. Sono potenti per l'elaborazione di stringhe e dati.
#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 — Generazione di range
views::iota genera una sequenza di valori incrementali. Può essere limitato (iota(0, 10) per 0..9) o illimitato (iota(0) per 0, 1, 2, ...). Combinato con take, sostituisce molti cicli di conteggio.
#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
}Costruire pipeline di dati complete
La vera potenza delle viste emerge quando componi più adattatori in una singola pipeline. L'intera pipeline valuta in modo lazy in un singolo passaggio.
#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';
}
}Le viste sono non proprietarie — fanno riferimento al range sottostante. Se il range sorgente viene distrutto, la vista diventa pendente:
auto make_view() {
std::vector<int> local = {1, 2, 3};
return local | std::views::filter([](int x) { return x > 1; });
// BUG: local viene distrutto, la vista diventa pendente!
}Fai attenzione anche alle viste su temporanei creati nel mezzo della pipeline:
// SBAGLIATO: la stringa è un temporaneo che viene distrutto
auto words = std::string("hello world")
| std::views::split(' '); // pendente!Assicurati sempre che i dati sorgente sopravvivano a tutte le viste che li referenziano. Memorizza i dati in una variabile con una durata sufficiente prima di creare viste su di essi.
Le viste sono lazy — nessun lavoro avviene fino all'iterazione. Questo significa:
1. Costruire una pipeline è essenzialmente gratuito (memorizza solo puntatori a funzione e parametri)
2. Puoi creare viste su range molto grandi o anche infiniti
3. Ogni elemento attraversa l'intera pipeline prima che l'elemento successivo venga elaborato
4. Le viste con short-circuit come take(n) fermano l'intera pipeline dopo n elementi
Per materializzare i risultati delle viste in un contenitore, itera e raccogli:
std::vector<int> results;
for (int x : some_view) results.push_back(x);
// C++23 aggiunge std::ranges::to per una materializzazione più pulita:
auto results = some_view | std::ranges::to<std::vector>();Preferisci mantenere i dati come viste il più a lungo possibile — materializza solo quando hai bisogno di accesso casuale, persistenza o per passare a API che richiedono contenitori.
- Le viste sono adattatori di range non proprietari, copiabili in O(1), con valutazione lazy
views::filtereviews::transformsono i mattoni fondamentali per le pipeline di dativiews::take(n)eviews::drop(n)forniscono slicing senza copiareviews::iotagenera sequenze — limitate o illimitate — sostituendo i cicli di conteggio- L'operatore pipe
|compone le viste in pipeline leggibili da sinistra a destra con zero allocazioni intermedie - Le viste non devono sopravvivere ai propri dati sorgente — assicura sempre che il range sottostante abbia una durata sufficiente
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. Quando una vista C++20 calcola i suoi elementi?
2. Qual è il risultato di `std::views::iota(1) | std::views::take(3)`?
3. Perché è pericoloso creare una vista su una variabile locale e restituirla da una funzione?