Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Le novità di C++11 (C++0x)

L'ultima edizione dello standard ISO C++, aggiunge modifiche così rilevanti da farlo definire "un nuovo linguaggio" da Bjarne Stroustrup, l'autore del linguaggio di programmazione
L'ultima edizione dello standard ISO C++, aggiunge modifiche così rilevanti da farlo definire "un nuovo linguaggio" da Bjarne Stroustrup, l'autore del linguaggio di programmazione
Link copiato negli appunti

C++11 è il nuovo standard ISO C++, approvato il 12 agosto 2011. Si tratta del terzo standard ufficiale del linguaggio di programmazione C++: mentre i due precedenti standard, C++98 e C++03, non presentavano modifiche rilevanti, C++11 introduce modifiche significative, tanto che Bjarne Stroustrup, autore del linguaggio di programmazione, lo ha definito quasi un nuovo linguaggio.

Le novità introdotte riguardano sia il core sia la libreria Standard. Le elenchiamo brevemente prima di affrontarle una ad una all'interno dell'articolo:

Le novità più importanti del core

  • espressioni lambda
  • auto e decltype
  • nuova sintassi di inizializzazione degli oggetti
  • costruttori delega
  • funzioni di default e deleted
  • nullptr
  • riferimenti rvalue

Le novità più importanti della libreria standard

  • la presenza di nuovi algoritmi
  • nuove classi contenitore
  • operazioni atomiche
  • type traits
  • espressioni regolari
  • nuovi smart pointers
  • async()
  • una libreria multithread

Nella prima parte dell'articolo ci occuperemo di introdurre brevemente le novità più importanti introdotte nel core mostrando un semplice esempio di utilizzo per ognuno di essi e specificando quali compilatori li supportano.

Gli esempi riportati sono stati testati con la versione 4.6 di g++ che dispone di una implementazione per la maggior parte dei nuovi costrutti; per la corretta compilazione è necessario utilizzare il flag -std=c++0x.

Le espressioni Lambda in C++

Una Lambda expression (lambda closure o espressione lambda) è una funzione senza nome definita al momento della chiamata. Il vantaggio principale di tale funzione consiste nell'eliminazione della chiamata a funzione quando si deve specificare una azione semplice.

È sconsigliata nei casi in cui l'azione che si vuole specificare non è né comune né semplice: in tal caso è raccomandabile utilizzare una funzione oppure un oggetto che incapsuli una funzione. Una espressione lambda ha la forma:

[] (parametri-di-input-della-funzione) (tipo-di-ritorno-della-funzione) {corpo-della-funzione;}

Per definire una funzione lambda è quindi necessario specificare:

  • una capture list (tra parentesi quadre '[]')che rappresenta la lista di variabili che è possibile utilizzare oltre agli argomenti della funzione. Nel caso in cui si definisca [&] tutte le variabili locali saranno passate per riferimento. Se non viene specificato niente, la lambda function comincia con [];
  • gli argomenti (opzionali) ed i loro tipi (ad esempio (int a, int b));
  • il tipo di ritorno della funzione (opzionale) . Se non viene definito, viene utilizzato void;
  • l'azione che deve essere eseguita tra parentesi graffe (ad esempio { return expression}).

Facciamo un esempio; supponiamo di voler contare le lettere maiuscole contenute in una stringa. Utilizzando for_each() per attraversare l'array di caratteri, è possibile utilizzare una lambda expression per determinare per ogni lettera se sia maiuscola o no. Nell'implementazione, ad ogni lettera maiuscola trovata viene applicata l'espressione lambda che incrementa una variabile definita all'esterno dell'espressione e passata come riferimento nella capture-list:

#include <vector> 
#include <iostream> 
#include <algorithm> 

int main() 
{ 
  char s[]="Hello World!"; 
  int Uppercase = 0; // variabile modificata dalla funzione lambda
  
  std::for_each(s, s+sizeof(s), [&Uppercase] (char c) { 
    if (isupper(c)) 
	  Uppercase++; 
  }); 
  
  std::cout<< Uppercase<<" uppercase letters in: "<< s<< std::endl; 
  
  return 0; 
} 

È come se fosse stata definita una funzione il cui corpo è interamente inserito all'interno di un'altra funzione. La capture list in questo caso è [&UpperCase] e, pertanto, il corpo della funzione lambda avrà a disposizione un riferimento alla variabile UpperCase in modo tale che possa modificarla. Senza l'utilizzo della &, la variabile UpperCase sarebbe stata passata per valore. Le espressioni lambda C++11 includono anche costrutti per le funzioni membro pure.

Tale costrutto è già supportato dai compilatori:

  • gcc 4.5 (1.1)
  • EDG eccp 4.1 (v0.9)
  • Intel C++ 11.0 (v1.0) e 12.0 (v1.0)
  • Micosoft Visual C 2010 (v1.0) e 2011.0 (v1.1)

auto e decltype

auto è una nuova parola chiave del linguaggio che consente di dichiarare oggetti senza doverne specificare esplicitamente il tipo qualora la dichiarazione dell'oggetto includa già un inizializzatore.

In tal caso l'utilizzo della parola chiave auto lascerà al compilatore il compito di trovare il tipo giusto dell'oggetto durante la fase di compilazione utilizzando l'inizializzatore. Di seguito sono riportati alcuni esempi:

auto x = 0; // x ha tipo int poiché 0 è int
auto c = 'a' // char
auto d = 0.5; // double

auto national_debt = 14400000000000LL;/ / long long

L'utilizzo di tale costrutto è utile quando il tipo di un oggetto ha un nome molto lungo o quando viene generato automaticamente (ad esempio nei template). Ad esempio la seguente istruzione

void func (const std::vector& vi)
{
  std::vector::const_iterator CI = vi.begin ();
}

potrebbe essere semplificata in questo modo:

void func (const std::vector& vi) 
{ 
  auto CI = vi.begin (); 
} 

omettendo la dichiarazione esplicita dell'iteratore. La parola chiave auto non è nuova, ma risale addirittura al periodo pre-ANSI C. Tuttavia, C++11 cambia il suo significato dato che in passato designava un oggetto con un tipo di memorizzazione automatica. Il significato storico di auto è stato rimosso da C++11 per evitare confusione.

Per fornire una funzionalità simile a quella offerta dal vecchio auto è stato introdotto il costrutto decltype, che consente di catturare il tipo di oggetti ed espressioni. Il nuovo operatore decltype prende in input una espressione e "restituisce" il suo tipo:

const std::vector<int> VI;
decltype typedef(vi.begin ()) CIT;
Another_const_iterator CIT;

Queste parole chiave sono supportate dai compilatori:

  • gcc 4.4 (1.0)
  • EDG eccp 4.1 (v1.0)
  • Intel C++ 11.0 (v1.0)
  • Microsoft Visual C 2011.0 (v1.1)
  • IBM xlc++ 11.1 (v1.0)
  • Clang (2.9)

Nuova sintassi di inizializzazione

C++11 introduce una nuova sintassi di inizializzazione degli oggetti che ha lo scopo di essere più uniforme di quanto non fosse nelle precedenti versioni dello standard. C++, infatti, ha almeno quattro notazioni di inizializzazione differenti, alcune delle quali si sovrappongono:

Inizializzazione utilizzando le parentesi tonde

std::string s("Stringa di test");
int m = int(); // inizializzazione di default

Notazione =:

std::string s = "Stringa di test";
int x = 5;

Notazione con parentesi graffe

Per le strutture dati di tipo POD (Plain Old Data Structure), ovvero struct o classi che fungono solo da contenitori di dati:

int arr [4] = {0,1,2,3};

Notazione per inizializzare i membri di una classe all'interno del costruttore

class S {
  int x;
  S (): x (0) {}
};

Differenze con C++03

Questa moltitudine di costrutti è spesso fonte di confusione, non solo tra i novizi. In aggiunta in C++03 non è possibile inizializzare array POD con new []. C++11 cerca di fare pulizia utilizzando una notazione con parentesi graffe uniforme per tutti gli utilizzi. Supponendo di avere la classe:

class C
{
  int a;
  int b;
  public: C (int i, int j);
};

la nuova sintassi di inizializzazione prevede l'utilizzo di:

C c {0,0} / / valido solo in C + 11

che in C++03 equivale a:

C c (0,0);

Il seguente esempio mostra invece la nuova sintassi per inizializzare un array con new (in passato non era possibile):

int *a = new int[3] {1, 2, 0}; // valido solo in C++11

class X {
  int a [4];
  public:
    X (): a {1,2,3,4} {} // C++11, inizializzatore di un membro di tipo array
};

Per quanto riguarda i contenitori, scomparirà la lunga lista di chiamate a push_back() per l'inserimento degli elementi. In C++ 11, infatti, è possibile inizializzare i contenitori in maniera più intuitiva come riportato nell'esempio che segue:

// C++11 inizializzatore contenitore
std::vector<std::string> vs = {"prima stringa", "seconda stringa", "terza stringa"}; 

std::map<std::string, std::string> persone = {{"Giuseppe Verdi", "ingegnere"}, 
                                              {"Mario Rossi", "avvocato"}};

La nuova sintassi degli inizializzatori è supportata solo da GCC a partire dalla versione 4.4.

C++11 supporta anche l'inizializzazione dei membri non statici all'interno della definizione nella classe come riportato nell'esempio che segue:

class C
{
  int a = 7; / / C + 11 solo
  public:
    C ();
};

Tale sintassi è supportata solo da:

  • GCC (dalla versione 4.7)
  • Clang (dalla versione 3.0)

Funzioni di default e deleted

Una funzione di default è una funzione della quale il compilatore genera una implementazione predefinita. Un esempio è quello riportato di seguito:

class A
{
  public:
    A () = default; // C + 11
};

Il valore =default indica esplicitamente al compilatore che sarà necessario generare l'implementazione predefinita per la funzione. Le funzioni di default hanno due vantaggi: sono più efficienti delle implementazioni manuali e liberano il programmatore dal compito di definire tali funzioni manualmente.

Al contrario, una funzione delete consente di eliminare le funzioni create automaticamente dal compilatore quando definiamo una classe. La sintassi è riportata di seguito:

int func () = delete;

Per esempio C++ dichiara automaticamente un costruttore di copia e un operatore di assegnazione per le classi. Per disabilitare la copia, dichiarare queste due funzioni membro speciali =delete:

class NOCOPY
{
public:
   NOCOPY & operator = (const NOCOPY &) = delete;
   NOCOPY (const NOCOPY &) = delete;
   NOCOPY() = default;
}; 
int main(void)
{
  NOCOPY a;
  NOCOPY b(a); // errore di compilazione: "error: use of deleted function 'NOCOPY::NOCOPY(const NOCOPY&)'"
  return 0;
}

Queste tipologie di funzioni sono supportate in:

  • GCC 4.4
  • EDG ecpp 4.1
  • Intel C++ 12.0
  • Clang 3.0

Nella seconda parte dell'articolo continueremo ad esaminare le novità del core, come l'innovazione del nullptr (il puntatore nullo), i costruttori delega e i riferimenti rvalue.

nullptr

nullptr è una nuova parola chiave del linguaggio che consente di indicare un puntatore nullo. nullptr è fortemente tipizzato e sostituisce le macro NULL e 0, spesso fonti di errore. Di seguito un esempio di utilizzo che mostra come nullptr renda meno ambigue alcune chiamate di funzione:

Se consideriamo le due funzioni:

void f(int); // # 1
void f(char *) ; // # 2

e chiamando in C++03 la funzione

f(0); // quale funzione sarà richiamata?

è poco chiaro quale delle due funzioni verrà effettivamente chiamata in quanto entrambe potrebbero essere compatibili con l'argomento passato.

In C++11, invece, la disponibilità di nullptr rende meno ambigua la chiamata che diventa:

f(nullptr) // non ambigua, chiama # 2

nullptr è applicabile a tutte le categorie di puntatori, sia puntatori a funzione che puntatori ad oggetti:

const char *pc = str.c_str(); // Dati puntatori
if (pc != nullptr)
  std::cout << pc << std::endl;
int (A::*pmf)() = nullptr; // puntatore ad una funzione membro
void (* PMF)() = nullptr; // puntatore a funzione

nullptr è supportato in:

  • GCC 4.6
  • Intel C++ 12.1
  • Microsoft Visual C 2010
  • Clang 2.9

Costruttori Delega

Il costruttore delega è un nuovo construtto che consente di chiamare un altro costruttore della stessa classe. Nelle precedenti versioni di C++ per avere due costruttori con lo stesso comportamento era necessario ripetere lo stesso codice codice oppure richiamare una funzione init().

Il primo approccio è soggetto ad errori e presenta problemi di manutenibilità mentre il secondo peggiora la leggibilità del codice. Il costruttore delega dovrebbe risolvere entrambi i problemi. Di seguito è riportato un esempio di utilizzo in cui sono presenti due costruttori.

#include <iostream> 
class M
{
  int x, y;
  char * p;
  public:
    M(int v): x(v), y(0), p(new char [1]) {} // # 1 costruttore target
    M(): M(0) {std::cout << "Costruttore delega" << std::endl;} // # 2 delegando
}; 
int main(void)
{
  M m;
  return 0;
}

Il costruttore #1 richiede un parametro di tipo int in ingresso mentre il secondo non richiede nessun parametro. Il costruttore #2 è proprio il costruttore delega: quando viene richiamato prima di tutto richiama il costruttore #1 dopo di chè vengono eseguite le istruzioni nel suo corpo.

Il costruttore delega è supportato da:

  • GCC (4.7)
  • IBM XLC++ (11.1)
  • Clang (3.0)

Riferimenti rvalue

I riferimenti rvalue sono dei riferimenti a valori rvalue. Per comprendere tale costrutto è necessario tenere bene in mente la differenza tra lvalue ed rvalue: si definisce lvalue ciò che può essere utilizzato dalla parte sinistra di una operazione di un assegnamento mentre rvalue designa ciò che può essere usato nel lato destro (da cui i nomi lvalue=left-value ed rvalue=right-value). Esempi di rvalue sono costanti e letterali.

In C++03 i riferimenti possono essere associati solo ad lvalue e non ad rvalue per prevenire la possibilità di modificare i valori di variabili temporanee che vengono distrutte prima che venga usato il loro valore. Ad esempio:

void incr(int& a) { ++a; }
int i = 0;
incr(i); // i becomes 1
incr(0); // errore: 0 non è un lvalue

Se fosse consentito sarebbe possibile anche modificare il valore di qualche dato temporaneo o, nel peggiore dei casi, il valore di 0 potrebbe diventare 1.

Il motivo principale per l'aggiunta di riferimenti rvalue è il costrutto move semantics. A differenza della copia tradizionale, la parola move sta a significare che un oggetto di destinazione ruba le risorse dell'oggetto di origine, lasciando la sorgente in uno stato "vuoto".

È utile per migliorare le prestazioni dato che è in grado di rendere più performanti molte operazioni. Ad esempio la copia di un oggetto può diventare più performante nei casi in cui possa essere sostituita da un'operazione di move (spostamento). Per apprezzare i vantaggi delle prestazioni della move semantics, si consideri una operazione di swap su una stringa. Un'implementazione base potrebbe essere la seguente:

void swap(std::string & a, std::string & b)
{
  std::string temp = a;
  a = b;
  b = temp;
}

Tale implementazione richiede l'esecuzione delle seguenti operazioni:

  • l'allocazione di nuova memoria
  • la copia di ogni carattere dalla stringa sorgente a quella di destinazione nelle tre operazioni di copia

In realtà in una operazione di swap non siamo interessati ad una vera e propria copia quanto piuttosto ad una operazione di spostamento da a a b. Utilizzando la move semantics è possibile eseguire l'operazione di copia con una sola operazione di swap tra i membri dei due dati. Un esempio di implementazione con il costrutto Move è il seguente:

void moveswap(std::string &a, std::string &b)
{
  std::string temp=move(a); // a potrebbe essere invalidato
  a=move(b); // b potrebbe essere invalidato
  b=move(temp); // temp potrebbe essere invalidato
}

Per implementare una classe che supporta tale costrutto è necessario:

  • dichiarare un costruttore di move
  • dichiarare un operatore di assegnamento move

Di seguito è riportato un esempio:

class Movable
{
public:
  Movable() =default;
  Movable (Movable&&); // costruttore move
  Movable && operator = (Movable&&); // operatore di assegnazione move
};

La libreria Standard C++11 utilizza la sematiche move in maniera estensiva per sfruttarne i vantaggi in termini di performance. Con l'introduzione di questo costrutto, infatti, molti algoritmi e contenitori sono stati ulteriormente ottimizzati.

Tale costrutto è supportato da:

  • GCC(4.3)
  • EDG eccp(4.1)
  • Intel C++(11,1 v1.0)
  • Microsoft Visual C 2010 (2.0)
  • C++ Builder 2009/2010
  • Clang


Ti consigliamo anche