Abstract
In questo post analizzeremo l’introduzione del C++20 di std::ranges e std::views e cercheremo di comprendere come questi elementi della STL modificano, semplificando, la stesura del codice.
Modello computazionale
Quesito
Vogliamo scrivere un algoritmo che prenda in input una collezione di numeri interi e in output restituisca solo quelli divisibili per 3 in ordine inverso.
Nella tabella sottostante troviamo alcuni esempi di input e output.
Input | Output |
---|---|
3 0 10 9 12 7 30 14 6 | 6 30 12 9 0 3 |
12 14 303 25 | 25 303 |
15 17 21 0 18 | 18 0 21 15 |
Soluzione pre C++20
|
|
Questo piccolo frammento di codice esegue i seguenti passaggi:
- Crea un std::vector tmp, di supporto.
- Copia tutti gli elementi di v che rispettino il funtore isDivisibleByThree in tmp.
- Inverte la sequenza di elementi di tmp.
Come si può constatare, il processo è lungo e tedioso, fortunatamente C++20 ha introdotto gli std::ranges.
Soluzione con std::ranges
|
|
La differenza è palese, tuttavia prima di entrare nello specifico, cerchiamo di approfondire il meglio concetto di ranges e di views.
Ranges
Range in italiano potrebbe essere tradotto con la parola intervallo.
Un range è l’astrazione di “una collezione di elementi” oppure “qualcosa di iterabile”.
Per definizione un range è una coppia di iteratori begin
e end
, il primo punta all’inizio di una collezione o sequenza e il secondo alla fine della predetta.
Possiamo quindi inferire che, per la precedente affermazione, ogni struttura dati (container) presente nella STL è un range.
Classificazione
I ranges possono essere classificati in modi differenti, tuttavia il più importante è in base alle capacità degli iteratori.
Avendo affrontato nel post precedente i concepts, possiamo riassumere i ranges nella tabella sottostante.
Concept | Descrizione |
---|---|
std::ranges::input_range | Può essere iterato dall’inizio alla fine almeno una volta |
std::ranges::forward_range | Può essere iterato dall’inizio alla fine molteplici volte |
std::ranges::bidirectional_range | L’iteratore può effettuare l’operazione -- (vai all’elemento precedente) |
std::ranges::random_access_range | Esiste l’operatore [] che permette l’accesso agli elementi in tempo costante |
std::ranges::contiguous_range | Gli elementi sono vincolati ad essere memorizzati contiguamente nella memoria |
Si nota facilmente la similitudine con i rispettivi concepts degli iteratori (std::forward_iterator ecc…).
Views
Ci sono tre concetti molto importanti che riguardano le views:
- Una view è un range.
- Una view non possiede i dati a cui accede.
- Una view applica le modifiche solo quando un elemento viene richiesto (lazy-evaluation).
Una view è un range
Per definizione, una view \(w\) è un range che viene definito su un’altro range \(r\). La view in questione può apportare modifiche sul range controllato tramite algoritmi e altre operazioni. (lazy-evaluation)
Una view non possiede i dati a cui accede
Quando si accede a un oggetto attraverso una view, si accede all’oggetto all’interno dell’indirizzo di memoria gestito dalla struttura datu su cui si basa la view.
Questo ha due implicazioni:
- Le viste sono veloci da creare, perché non hanno bisogno di copiare i dati sottostanti.
- Le modifiche apportate dalla view non si ripercuotono sul contenitore originale.
|
|
Come si può notare la view non ha modificato il range numbers. Tuttavia però è vero il contrario, ossia, modificando il contenitore originale il cambiamento si ripercuote su tutte le view che usano questo range.
Avremo pertanto
|
|
Lazy-evaluation
Altra concetto da tenere bene a mente è che una view applica le modifiche richieste solo nel momento in cui un elemento viene richiesto e non al momento della creazione della view stessa. Questo garantisce un’ottima flessibilità e permette che una view possa essere usata flessibilmente come un iteratore. Tuttavia se una view dovesse computare una trasformazione
|
|
Composizioni e Pipelines
Alcuni potrebbero chiedersi il perchè abbia scritto
|
|
Anzichè utilizzare
|
|
Questo perchè std::views::reverse non è una view, bensì è un adattatore che prende il sottostante range, std::vector in questo caso, e restituisce un oggetto view sul std::vector. Il tipo esatto di view viene nascosto dietro la keyword auto, in questo modo otteniamo il vantaggio di non doversi preoccupare di compilare gli argomenti del template della view. Un ulteriore pregio di questa dichiarazione è la possibilità di concatenare adattatori multipli con dei pipe.
Per esempio anzichè utilizzare
|
|
Potremo scrivere
|
|
Esempi
Vogliamo creare una view dei primi 5 elementi di un std::vector e stampare il risultato.
|
|
Vogliamo sfruttare un algoritmo range based per stampare un std::vector invertito e ripulito da tutti i valori negativi.
|
|
Concetti avanzati
Range Factories
La libreria standard mette a disposizione delle range factories, algoritmi, che forniscono la possibilità di creare delle view senza richiedere un range sottostante.
Una tra le svariate è std::views::iota, che crea una view incrementale di interi.
|
|
Zip Views
La funzionalità avanzata std::views::zip(), introdotta in C++23, permetta di combinare più range (o view) in un’unica view. Per ottenere ciò, la libreria sfrutta un’altra componente della STL, std::tuple. Ogni elemento nella zip view è una tupla, pertanto gli elementi di ciascuna view sono contenuti parallelamente.
Cerchiamo di capire meglio con un esempio.
|
|
Conclusione
Anche questo articolo è terminato, tuttavia per quanto io possa essere entrato nei dettagli, mi sembra di non essere stato esaustivo.
Per tutti coloro che fossero interessati ad approfondire questa ranges library
, al seguente link troverete la conoscenza che tanto bramate.