Range C++20: un nuovo paradigma
Scopri i range C++20 — una riprogettazione moderna dell'interfaccia degli algoritmi STL. Impara i concept dei range, gli algoritmi basati su range, le proiezioni e l'operatore pipe per trasformazioni dati componibili.
Perché i range?
I Range C++20 sono un aggiornamento fondamentale del modello degli algoritmi della STL. La STL tradizionale richiede di passare coppie di iteratori a ogni algoritmo, il che è verboso e soggetto a errori (puoi accidentalmente passare iteratori da contenitori diversi). I range risolvono questo problema permettendoti di passare il contenitore stesso — o qualsiasi oggetto che definisca begin() e end().
Miglioramenti chiave rispetto agli algoritmi tradizionali:
1. Passa i contenitori direttamente — std::ranges::sort(vec) invece di std::sort(vec.begin(), vec.end())
2. Proiezioni — trasforma gli elementi prima del confronto, senza modificarli
3. Concept — gli algoritmi hanno requisiti chiari a tempo di compilazione
4. Componibilità — concatena le operazioni con l'operatore pipe |
5. Valutazione lazy — le viste calcolano gli elementi su richiesta, evitando copie intermedie
Concept dei range
C++20 definisce una gerarchia di concept dei range in :
- std::ranges::range — ha begin() e end()
- std::ranges::sized_range — ha size() in O(1)
- std::ranges::input_range — può essere letto una volta (come gli iteratori di input)
- std::ranges::forward_range — attraversamento forward multi-passaggio
- std::ranges::bidirectional_range — forward + all'indietro
- std::ranges::random_access_range — bidirezionale + salti O(1)
- std::ranges::contiguous_range — accesso casuale + memoria contigua
- std::ranges::common_range — begin() e end() restituiscono lo stesso tipo
- std::ranges::viewable_range — può essere convertito in una vista
Questi concept vengono usati per vincolare gli algoritmi basati su range, fornendoti messaggi di errore chiari quando passi un range incompatibile.
Algoritmi basati su range
Gli algoritmi basati su range risiedono in std::ranges:: e accettano interi contenitori. Restituiscono anche tipi di risultato più ricchi anziché semplici iteratori.
#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';
}
}Proiezioni — Trasforma prima di confrontare
Le proiezioni sono una caratteristica unica degli algoritmi basati su range. Ti permettono di specificare come "vedere" ogni elemento per il confronto o l'elaborazione, senza modificare l'elemento stesso.
#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
}L'operatore pipe per la composizione
L'operatore pipe | è il segno distintivo dei range C++20. Ti permette di concatenare gli adattatori di range (viste) in una pipeline che si legge da sinistra a destra, come una pipe della shell Unix:
auto result = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; })
| std::views::take(5);Si legge naturalmente: "prendi i numeri, mantieni quelli pari, elevali al quadrato, prendi i primi 5." La pipeline è lazy — nessun calcolo avviene finché non iteri su result. Questo significa che non vengono allocati vector intermedi.
Esploreremo le viste in dettaglio nella prossima lezione.
Gli algoritmi dei range proteggono contro gli iteratori pendenti da range temporanei. Se passi un contenitore temporaneo, l'iteratore restituito sarebbe pendente perché il temporaneo viene distrutto al punto e virgola:
auto it = std::ranges::find(get_vector(), 42);
// Restituisce std::ranges::dangling invece di un iteratore!
// Tentare di dereferenziarlo non compilerà.Questa è una caratteristica di sicurezza a tempo di compilazione — il tradizionale std::find restituirebbe silenziosamente un iteratore pendente, portando a comportamento indefinito a runtime. Il tipo sentinella std::ranges::dangling lo rende un errore di compilazione.
Preferisci gli algoritmi std::ranges:: nel nuovo codice — sono più sicuri (nessun iteratore non corrispondente, protezione da dangling), più leggibili (passa i contenitori direttamente) e più potenti (proiezioni, concept).
Usa gli algoritmi tradizionali std:: quando:
- Il tuo codebase deve supportare compilatori pre-C++20
- Hai bisogno di un overload specifico che i range non forniscono
- Lavori con coppie di iteratori da API legacy
Nota: gli algoritmi std::ranges:: non sono sostituti drop-in. Alcuni restituiscono tipi diversi (ad es., std::ranges::sort restituisce l'iteratore alla fine dell'intervallo ordinato). Consulta la documentazione quando effettui la migrazione.
- I range C++20 ti permettono di passare i contenitori direttamente agli algoritmi — niente più boilerplate
begin()/end() - Le proiezioni trasformano gli elementi per il confronto senza modificare i dati originali
- I concept dei range (
input_range,random_access_range, ecc.) forniscono vincoli chiari a tempo di compilazione - L'operatore pipe
|abilita pipeline di elaborazione dati componibili, da sinistra a destra std::ranges::danglingpreviene bug da iteratori pendenti a tempo di compilazione- Preferisci gli algoritmi
std::ranges::nel nuovo codice C++20 per sicurezza, leggibilità e potenza
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. Cos'è una proiezione nel contesto degli algoritmi range C++20?
2. Cosa restituisce `std::ranges::find` quando viene chiamato con un contenitore temporaneo?
3. Cosa fa `std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)`?