La libreria standard C++ subì già un importante restyling nel 2003 con la Library Technical Report 1 (TR1) che introdusse nuove classi contenitore (tra cui unordered_set
, unordered_map
, unordered_multiset
e unordered_multimap
) e nuove librerie (tra cui quella dedicata alle espressioni regolari ed alle tuple). Con l'approvazione di C++11, TR1 viene incorporata ufficialmente nello standard C++, insieme a nuove librerie aggiunte dopo TR1.
Tale aggiunta sancisce l'entrata di tali funzionalità nel namespace standard (std
) mentre in passato erano presenti nel namespace TR1 (std::tr1
).
Le versioni iniziali delle nuove librerie standard vengono già distribuite nelle ultime implementazioni di GCC, CLang e Microsoft e sono disponibili anche dal progetto Boost. Al contrario delle funzionalità del core non è disponibile una matrice delle compatibilità in cui è riportata la compatibilità nativa dei singoli compilatori. Il codice presente in questo articolo è stato testati con g++ 4.6.
Vediamo subito quali sono le novità più importanti nella libreria standard C++11.
Nuovi algoritmi e funzioni su insiemi
La libreria Standard C++11 definisce nuovi algoritmi tra cui i più importanti sono:
- algoritmi che simulano le operazioni sugli insiemi
- algoritmi copy_n
- algoritmo iota()
Operazioni sugli insiemi
I nuovi algoritmi per le operazioni su insiemi sono all_of
, any_of
e none_of
e consentono di verificare se un predicato è verificato rispettivamente da tutti gli elementi di un insieme, solo da alcuni o da nessuno.
Tutti e tre richiedono tre argomenti:
- il primo rappresenta un iteratore al primo elemento da considerare
- il secondo un iteratore all'ultimo elemento da considerare
- il terzo rappresenta il predicato, ovvero la condizione che si desidera verificare sugli elementi dell'insieme compresi tra il primo iteratore e il secondo
La funzione all_of verifica se il predicato sia vero su tutti gli elementi, la funzione any_of verifica se sia valido su alcuni mentre none_of verifica se non sia valido su nessuno. Tutte e tre le funzioni possono essere utilizzate con qualsiasi contenitore che accetti un iteratore.
Ecco un semplice esempio in cui il predicato isPositive
viene verificato tramite questi tre nuovi algoritmi su un vettore.
#include <algorithm>
#include <vector>
#include <iostream>
bool ispositive(int i) { if(i>0) return true; }
int main(int argc, char **argv)
{
std::vector <int> myVector = {-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
bool allPositive = all_of(myVector.begin(), myVector.end(), ispositive); // false
bool anyPositive = any_of(myVector.begin(), myVector.end(), ispositive); // true
bool nonePositive = none_of(myVector.begin(), myVector.end(), ispositive); // false
std::cout << "AllPositive: " << allPositive << std::endl;
std::cout << "AnyPositive: " << anyPositive << std::endl;
std::cout << "NonePositive: " << nonePositive << std::endl;
return 0;
}
Output:
AllPositive: 0 AnyPositive: 1 NonePositive: 0
copy_n
Gli algoritmi copy_n consentono di copiare più elementi da un contenitore a un altro. La funzione copy_n richiede tre parametri:
- il primo è un iteratore al primo elemento di un contenitore dal quale si deve
cominciare a copiare (oppure un puntatore); - il secondo rappresenta il numero di elementi consecutivi che si desidera copiare;
- il terzo un iteratore al contenitore di arrivo in cui sarà effettuata la copia.
Il seguente codice, ad esempio, copia un array di 5 elementi in un altro array di 5 elementi:
#include <algorithm>
int main(int argc, char **argv)
{
int source [5] = {0,12,34,50,80};
int target [5];
// Copia 5 elementi dall'origine alla destinazione
std::copy_n(source, 5, target);
return 0:
}
iota
La funzione iota è utile ad inizializzare un array con una sequenza di valori crescenti e
sequenziali, richiede tre parametri:
- un iteratore al primo elemento di un contenitore;
- un iteratore all'ultimo elemento;
- il valore di partenza.
iota
assegna al primo elemento del contenitore specificato il valore indicato come ultimo parametro ed ai successivi un valore via via crescente incrementato utilizzando il prefisso ++
.
Nell'esempio che segue, iota
assegna i valori consecutivi {10,11,12,13,14}
all'array a
, e {'a', 'b', 'c'}
all'array di caratteri c
.
#include
int main(int argc, char **argv)
{
int a[5] = {0};
char c[3] = {0};
std::iota (a, a+5, 10); // a diventa {10,11,12,13,14}
std::iota (c, c+3, 'a'); // {'a', 'b', 'c'}
return 0;
}
Nuove classi contenitore
Le nuove classi contenitore erano già presenti nella TR1 e adesso entrano ufficialmente nella libreria standard.
std::array
std::array è un nuovo contenitore standard di dimensioni prefissate. Al differenza di std::vector
può ospitare solo un numero di elementi definito in fase di inizializzazione e non può crescere o decrescere.
In altre parole non espone i metodi push_back
e pop_back
per aggiungere o rimuovere elementi.
È molto simile ad un array C, ma a differenza di quest'ultimo, non è implicitamente un vero e proprio puntatore. Qualora necessario è possibile ottenere un puntatore ad un std::array ma deve essere richiesto esplicitamente con una funzione apposita.
Nel codice che segue vediamo un esempio di uso di questo contenitore e il suo rapporto con i puntatori:
#include <array>
int main(int argc, char **argv)
{
std::array<int,6> a = {1, 2, 3};
a [3] = 4;
int x = a [5]; // x diventa 0 perché gli elementi di default sono inizializzati a zero
// int *p1 = a; // errore: std::array non è implicitamente un puntatore
int *p2 = a.data (); // ok: ottenere puntatore al primo elemento
return 0;
}
Anche se può esistere uno std::array di qualunque lunghezza (anche zero) non possiamo definire la dimensione di un array implicitamente tramite un elenco di inizializzazione. Vediamo nel prossimo esempio alcune problematiche che potrebbero nascere nel caso in cui non venga specificata la lunghezza ma ci sia
solo un elenco di inizializzatori:
// std::array<int> a3 = {1, 2, 3}; // errore: dimensioni sconosciute/mancanti
std::array<int,0> a0; // ok: nessun elemento
// int * p = a0.data (); // comportamento non specificato; da evitare!
Gli std::array sono stati pensati per essere utilizzati nello sviluppo di sistemi embedded ed altre applicazioni con vincoli di prestazioni e sicurezza. Per il resto std::array
mette a disposizione le funzioni tipiche dei contenitori.
Un'altro vantaggio di std::array rispetto agli array C è che si evitano problemi con le conversioni di base derivate.
std :: forward_list
std::forward_list è un nuovo contenitore standard, definito nel file di inclusione <forward_list>
, e consente di rappresentare una linked-list singola.
A differenza degli altri iteratori consente solo l'iterazione in avanti, l'inserimento di nuovi elementi dopo un elemento esistente (funzione insert_after
) o la rimozione di elementi dopo un elemento
esistente (funzione erase_after
).
L'occupazione di spazio è minima (una lista vuota dovrebbe occupare solo una word) e non fornisce una funzione size
dato che non ha un membro che possa conservare la dimensione della lista. Al contrario dei contenitori usuali non dispone di una funzione back
per recuperare l'ultimo elemento né di una funzione push_back
per inserire un elemento alla fine.
Ecco un semplice esempio di utilizzo che mostra il funzionamento dela funzione insert_after
:
#include <iostream>
#include <array>
#include <forward_list>
int main ()
{
// Inizializza una linked-list ed il suo iteratore
std::forward_list<int> mylist;
std::forward_list<int>::iterator it;
// Esempi di utilizzo di insert_after
it = mylist.insert_after ( mylist.before_begin(), 1 ); // 10
it = mylist.insert_after ( it, 3, 10 ); // 10 20 20
it = mylist.begin(); // ^
it = mylist.insert_after ( it, {2,3,4} ); // 10 1 2 3 20 20
// Print list elements
std::cout << "La lista contiene:";
for (int& x: mylist)
std::cout << " " << x;
std::cout << std::endl;
return 0;
}
Output:
La lista contiene: 1 2 3 4 10 10 10
Contenitori unordered (hash)
Un contenitore unordered è una sorta di hash table. C++11 mette a disposizione contenitori unordered di quattro tipologie differenti:
- unordered_map
- unordered_set
- unordered_multimap
- unordered_multiset
Il nome di questi contenitori avrebbe dovuto essere 'hash', ad esempio avremmo avuto 'hash_map' piuttosto che unordered_map
ma in tal caso ci sarebbero stati problemi di incompatibilità, perciò il comitato di standardizzazione è stato costretto a scegliere un nuovo nome e unordered_map
è sembrato il migliore.
L'aggettivo 'unordered' si riferisce a una delle differenze principali tra una map ed una unordered_map: quando si scorre una mappa è possibile iterare seguendo un certo ordine fornito dall'operatore minore di confronto (per default <) dei tipi che contiene; nel caso di unordered_map
, invece, non è necessario avere un operatore minore di confronto ed una hashmap non deve necessariamente fornire un ordine. Viceversa, il tipo di elemento di una mappa non è richiesto che abbia una funzione di hash.
L'idea alla base di tale costrutto è fornire una versione ottimizzata di una mappa laddove possibile. Ad esempio:
#include <map>
#include <unordered_map>
#include <string>
#include <iostream>
int main(int argc, char **argv)
{
std::map<std::string,int> myMap {{"Verdi",1979}, {"Rossi",1986}};
myMap["Argese"] = 1984;
for(auto x : myMap)
std::cout << '{' << x.first << ',' << x.second << '}';
std::unordered_map<std::string,int> myUnMap {{"Verdi",1979}, {"Rossi",1989}};
myUnMap["Argese"] = 1984;
for(auto x : myUnMap)
std::cout << '{' << x.first << ',' << x.second << '}';
return 0;
}
Il primo ciclo for che scorre la map
presenterà gli elementi in ordine alfabetico mentre il secondo for che cicla sulla unordered_map
non sarà ordinato.
La funzione di lookup è implementata in maniera molto diversa per i due contenitori: per la ricerca all'interno di una mappa sono necessarie log2 (m.size ())
operazioni di confronto, mentre per la ricerca all'interno di una unordered_map è necessaria una chiamata di una funzione hash e una o più operazioni di uguaglianza.
Nel caso in cui siano presenti pochi elementi (ad esempio qualche decina), è difficile dire quale delle due implementazioni risulti più veloce. Per un maggior numero di elementi (ad esempio migliaia) la ricerca in
un unordered_map
dovrebbe essere molto più veloce piuttosto che per una mappa dato che ha una complessità computazionale più bassa.
std::tuple
std::tuple è una sequenza ordinata di N valori (una N-tupla) in cui N può essere una costante tra 0 e un valore molto grande definito dalla specifica implementazione nel file di inclusione <tuple>. Si può pensare ad una tupla come una struct
senza nome che contiene un certo numero di membri il cui tipo è specificato nella fase di inizializzazione della tupla.
Gli elementi di una tupla sono memorizzati in maniera compatta. I tipi di elementi di una tupla possono essere specificati in modo esplicito o possono essere dedotti (utilizzando la funzione make_tuple
); è possibile accedere ai singoli elementi tramite un indice (che parte
da 0
) utilizzando la funzione std::get
:
#include <tuple>
#include <string>
int main(int argc, char **argv)
{
// Esempi di inizializzazione di una tupla
std::tuple<std::string,int> t2("Rossi",154);
// t avrà il tipo std::tuple<string,int,double>
auto t = make_tuple(std::string("Verdi"), 10, 1.23);
// Esempi di accesso ai dati di una tupla
std::string s = std::get<0>(t);
int x = std::get<1>(t);
double d = std::get<2>(t);
return 0;
}
Le tuple vengono utilizzate tutte le volte che si desidera una lista eterogenea di elementi in fase di compilazione, ma si vuole evitare di definire una classe per contenerli. Ad esempio, std::tuple
si usa all'interno di std::function
e std::bind
per gli argomenti.
La tupla utilizzata più di frequente è la tupla con due elementi. Tuttavia, tale tupla è supportata direttamente nella libreria standard tramite std::pair. Da C++11 in poi std::pair potrà essere considerato una specializzazione della tupla tanto è vero che può essere utilizzato per inizializzare una tupla mentre, chiaramente, non è possibile il viceversa.
Link utili