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()
- 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
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:
La funzione all_of any_of none_of
Ecco un semplice esempio in cui il predicato isPositive
#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
++
Nell'esempio che segue, iota
{10,11,12,13,14}
a
{'a', 'b', 'c'}
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
unordered_map
L'aggettivo 'unordered' si riferisce a una delle differenze principali tra una map ed una unordered_map
unordered_map
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
unordered_map
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 ())
ricerca all'interno di una unordered_map
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
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
std::function
std::bind
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.