Vai al contenuto

C++11

Da Wikipedia, l'enciclopedia libera.
Voce principale: C++.

Il C++11, conosciuto precedentemente come C++0x[1], è uno standard per il linguaggio di programmazione C++ che ha sostituito la revisione del 2003 (C++03). Questo standard comprende numerose novità per il core del linguaggio ed estende la libreria standard[2] incorporando molte delle librerie del cosiddetto TR1 (il “C++ Standards Committee's Library Technical Report”).

Il “C++ Standards Committee” ha completato il nuovo standard nel settembre 2008 e la bozza è stata presentata con il nome di N3126 il 21 agosto 2010. Il 25 marzo 2011 l'ISO ha votato la bozza finale (targata N3290) che è stata contrassegnata come FDIS (Final Draft International Standard)[3]. Il 1º settembre 2011 è stata pubblicata la versione finale del C++11 da parte di ISO e IEC (ISO/IEC 14882:2011(E) Programming Languages—C++, Third Edition.)[4][5][6] Molte software house e progetti open source stanno sviluppando vari compilatori già funzionanti[7].

Una delle ragioni che spingono ad un processo evolutivo un linguaggio di programmazione come il C++ è la necessità di poter programmare più velocemente, elegantemente e, soprattutto, ottenendo un codice la cui manutenzione sia agevole. Questo processo porta inevitabilmente verso l'incompatibilità con il vecchio codice, per questo motivo, durante il processo di sviluppo del C++, ogni tanto si sono presentate alcune incompatibilità con le versioni precedenti. Secondo quanto annunciato da Bjarne Stroustrup (inventore del linguaggio C++, nonché membro del comitato), questo standard ha mantenuto pressoché al 100% la compatibilità con lo standard precedente[8].

I maggiori benefici non arrivano da soluzioni che permetteranno di scrivere meglio una linea individuale di codice, ma da quelle soluzioni che consentono al programmatore di risolvere un problema ed organizzare meglio il codice; così come avvenuto con l'introduzione della programmazione orientata agli oggetti ed alla programmazione generica (i template).

Le modifiche annunciate per l'imminente aggiornamento dello standard

[modifica | modifica wikitesto]

Come detto le modifiche allo standard C++ riguarderanno sia il linguaggio di programmazione e sia la libreria standard.

Nello sviluppo di ogni utility del nuovo standard, il comitato ha applicato alcune direttive:

  • Mantenere stabilità e compatibilità con il C++98 e possibilmente anche con il C;
  • Preferire l'introduzione di nuove risorse attraverso la libreria standard, anziché estendere il core del linguaggio;
  • Prediligere le modifiche che possono evolvere il modo di programmare;
  • Rendere il C++ un linguaggio migliore per la programmazione di sistemi e librerie, piuttosto che introdurre nuove risorse dedicate soltanto ad applicazioni particolari;
  • Incrementare la sicurezza dei tipi fornendo soluzioni sicure alle correnti soluzioni insicure;
  • Incrementare le performance e le capacità di lavorare direttamente con l'hardware;
  • Fornire soluzioni adatte al mondo reale;
  • Attuare il principio “zero-overhead” (il supporto addizionale richiesto per alcune utility va impiegato solo se la utility è effettivamente utilizzata dal codice);
  • Rendere il C++ facile da insegnare e da apprendere a vantaggio dei principianti, senza rimuovere nessuna particolarità necessaria ai programmatori esperti.

L'attenzione verso i principianti è molto importante, sia perché sono e saranno sempre la maggioranza rispetto agli esperti, sia perché molti principianti non intendono approfondire la loro conoscenza del C++, limitandosi ad operare nei campi in cui sono specializzati.

Estensioni al Linguaggio di programmazione C++

[modifica | modifica wikitesto]

Le attenzioni migliori del comitato per il C++ sono rivolte allo sviluppo del core del linguaggio. È sull'avanzamento dei lavori di questa parte dello standard che dipende la data di presentazione del C++0x.

Spesso si critica il C++ per la gestione non sicura dei tipi. Ma il C++, anche con il prossimo standard, non potrà diventare completamente sicuro nella gestione dei tipi di dato (come il Java), perché, per fare un esempio, comporterebbe l'eliminazione dei puntatori non inizializzati e quindi snaturerebbe tutto il linguaggio. Nonostante ciò sono in molti ad insistere perché lo standard C++ preveda dei meccanismi per la gestione sicura dei puntatori. Per questo motivo il nuovo standard fornirà un supporto per gli smart pointer, ma solo attraverso la libreria standard.

Le aree dove il core del C++ sarà molto migliorato sono quelle per un miglior supporto al multithreading, alla programmazione generica, ed a meccanismi di costruzione ed inizializzazione più flessibili.

Utilità per il Multitasking

[modifica | modifica wikitesto]

Il comitato del C++ ha intenzione di introdurre per il prossimo standard degli strumenti per la programmazione in multiprocessing e per la programmazione in multithreading.

Un supporto completo per il multiprocessing appare, per il momento, troppo dipendente dal sistema operativo utilizzato e troppo complesso, per essere risolto solo attraverso un'estensione del linguaggio. Si ritiene che il supporto alla programmazione tra processi dovrebbe essere realizzato mediante una libreria di alto livello, da preferirsi rispetto ad una libreria di basso livello (con primitive di sincronizzazione potenzialmente pericolose), poiché il comitato non intende incentivare il programmatore ad utilizzare una libreria standard potenzialmente pericolosa, invece di una libreria sicura anche se non standardizzata.

Nel prossimo standard verranno aggiunti al core del C++ alcune utilità per il multithreading, mentre lo sviluppo di una libreria per i thread resta un obiettivo meno prioritario per il comitato.

Esecuzione parallela

[modifica | modifica wikitesto]

Sebbene la proposta vada ancora definita, è possibile che venga introdotto nel prossimo standard (o in futuro) un meccanismo per implementare il costrutto cobegin e coend, utilizzando i thread.

Per il momento non ha molta importanza quale keyword verrà utilizzata per introdurre la nuova notazione. È importante, invece, farsi un'idea di come sarà facile, mediante poche linee di codice, implementare blocchi di istruzioni eseguibili in parallelo.

active
{
  // Primo blocco.
  {
    // ...
  }
  // Secondo blocco.
  for( int j = N ; j > 0 ; j-- )
  {
    // ...
  }
  // Terzo blocco.
  ret = funzione(parametro) ;
  // Altri blocchi.
  // ...
}

Tutti i vari blocchi vengono eseguiti in parallelo. Dopo che l'esecuzione di ognuno è terminata, il programma riprende la sua esecuzione con un singolo thread.

Chiamata di funzione asincrona

[modifica | modifica wikitesto]

Un'altra proposta ancora da definire, che potrebbe essere introdotta nel prossimo standard (o in futuro), è un meccanismo, mediante thread, per effettuare una chiamata asincrona ad una funzione.

La sintassi probabilmente assomiglierà a quella dell'esempio successivo:

int funzione( int parametro ) ;
// Effettua la chiamata e ritorna immediatamente.
IdThreadType<funzione> IdThread = future funzione(parametro) ;
// Attende il risultato della chiamata.
int ret = wait IdThread ;

Thread-Local Storage

[modifica | modifica wikitesto]

Spesso in ambiente multithread è necessario possedere delle variabili uniche per ogni thread. Questo già avviene per le variabili locali delle funzioni, mentre non avviene per le variabili globali o statiche.

È stato proposto, per il prossimo standard, un nuovo “specificatore di classe di memorizzazione”, che si aggiunge ai numerosi già esistenti (extern, static, register, auto e mutable). Il nuovo specificatore dovrebbe chiamarsi semplicemente thread (al momento nella documentazione ufficiale viene utilizzato __thread).

Gli oggetti thread sono simili agli oggetti globali. Mentre gli oggetti globali hanno un campo di esistenza che copre tutta l'esecuzione del programma, gli oggetti thread hanno un campo di esistenza limitato ad un singolo thread, al cui termine la variabile non può essere più acceduta. Un oggetto thread può essere inizializzato come una qualsiasi variabile di durata statica, eventualmente impiegando un costruttore; se possiede un distruttore, questo verrà chiamato al termine del thread.

Operazioni atomiche

[modifica | modifica wikitesto]

Nella multiprogrammazione non c'è solo il problema della sincronizzazione tra i vari thread, spesso un thread ha la necessità di eseguire delle operazioni senza che nessuno lo interrompa, questo può avvenire ed esempio nei sistemi Real-Time quando bisogna operare con una periferica, oppure quando il thread necessita un accesso esclusivo a tutte le variabili globali.

Per eseguire senza interruzioni una serie di operazioni (dette atomiche) è stata proposta la nuova keyword atomic, utilizzabile come nell'esempio seguente:

atomic
{
  // Operazioni atomiche.
  ...
}

Un nuovo significato per il modificatore di accesso volatile

[modifica | modifica wikitesto]

Nelle versioni di C++ precedenti all'undicesima, il modificatore di accesso volatile informa il compilatore che il valore di una variabile può essere modificato in qualsiasi momento, indipendentemente dalla volontà del thread; per esempio se la variabile è un registro di un dispositivo memory mapped. L'utilizzo di volatile è importante perché limita le ottimizzazioni del compilatore, altrimenti esso assumerebbe che la variabile possa essere modificata solo nel lato sinistro di un'istruzione di assegnamento.

È stato proposto di utilizzare la keyword volatile per indicare anche gli oggetti destinati alla comunicazione tra thread, o comunque condivisi tra più thread. In modo che la scrittura e la lettura di questi oggetti venga protetta automaticamente dal compilatore, mediante sincronizzazione, per evitare errori derivati da un accesso concomitante. Tuttavia questa soluzione al momento appare inappropriata se applicata ad oggetti complessi, ed è ancora in fase di discussione.

Utilità per le Classi

[modifica | modifica wikitesto]

L'enfasi sulle caratteristiche generali delle classi del C++ è stato il motore principale per il successo di questo linguaggio di programmazione. Per questo è facile immaginare come le innovazioni più significative al core del C++ siano proprio dedicate allo sviluppo di nuove utilità rivolte ad incrementare le performance delle classi.

Operatori di conversione espliciti

[modifica | modifica wikitesto]

L'utilizzo implicito degli operatori di conversione di tipo rappresenta una delle insidie più frequenti per un programmatore. Per questo motivo viene consigliato di utilizzare il più possibile operatori di conversione come lo static_cast.

Una possibile soluzione a questo problema sarebbe l'eliminazione del cast implicito da parte del compilatore come suggerito da alcuni puristi (e magari anche del cast C-style, anch'esso fonte di errori). Ma questa soluzione non è praticabile, perché comporterebbe problemi di compatibilità e renderebbe il linguaggio C++ troppo verboso. Per il momento si è pensato di introdurre gli “operatori di conversione espliciti”, questi, a differenza degli operatori di conversione generici, possono essere invocati soltanto attraverso il cast C-style e lo static_cast. L'esempio successivo chiarisce la sintassi e l'utilizzo della nuova utility.

class A  {} ;
class B  {} ;
class C
{
  public:
             operator A() ;  // Utilizzabile anche implicitamente.
    explicit operator B() ;  // Utilizzabile solo esplicitamente.
} ;

A a ;
B b ;
C c ;
a = c ;  // OK.
b = c ;  // ERRORE: non esiste un operatore di
         // conversione utilizzabile implicitamente.
b = static_cast<B> (c) ;  // OK, conversione in stile C++.
b = (B)c ;  // OK, conversione in stile C.

Sequence constructors

[modifica | modifica wikitesto]

L'idea di base è quella di permettere l'inizializzazione dei vettori di oggetti definiti dall'utente, facile tanto quanto l'inizializzazione dei vettori di tipi predefiniti, come nell'esempio:

int vettore[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, } ;

Se invece abbiamo una classe simile alla seguente, nello standard C++98 non esiste la possibilità di inizializzare in modo così diretto e chiaro i suoi membri:

 class vettore
{
  private:
    int elementi[10] ;

  public:
    vettore(???)  {???}
} ;

L'idea è quella di introdurre un costruttore speciale a partire da una sequenza di oggetti:

 vettore( initializer_list<int> seq ) ;  // Costruttore da una sequenza di int.

L'inizializzatore di sequenza initializer_list è in realtà una classe template definita come segue:

 template<class E> class initializer_list
 {
   public:
     initializer_list( const E* first, const E* last   ) ; 
     initializer_list( const E* first, int      length ) ;
 
     int      size () const ;  // Numero di elementi.
     const E* begin() const ;  // L'indirizzo del primo elemento.
     const E* end  () const ;  // L'indirizzo successivo all'ultimo elemento.
 } ;

Ecco una possibile definizione del costruttore di sequenza della classe vettore:

vettore::vettore( initializer_list<int> seq )
{
  int j = 0 ;
  for( int *indice = seq.begin() ; j < 10 && indice < seq.end() ; indice++ )
  {
    elementi[j++] = *indice ;
  }
  while( j < 10 )
  {
    elementi[j++] = 0 ;
  }
}

Costruttori delegati

[modifica | modifica wikitesto]

Una classe può possedere molti costruttori e spesso questi debbono inizializzare allo stesso modo variabili e classi base ereditate. Capita altrettanto spesso che non sia possibile scrivere una funzione di inizializzazione, perché le classi base ed i riferimenti (&) vanno inizializzati prima di poter accedere al corpo del costruttore. Inoltre il riassegnamento è sconsigliabile per le classi ereditate e impossibile per i riferimenti. Questo porta il programmatore a dover ripetere più volte le stesse istruzioni per ogni costruttore, con risultati disastrosi sul fronte del mantenimento del codice.

È stato pertanto proposto che i costruttori possano richiamare altri costruttori, delegando a questi ultimi il compito di inizializzare l'oggetto:

class coordinata
{
  private:
    coordinata( int ini_cx, int ini_cy, int ini_cz ):
                cx(ini_cx), cy(ini_cy), cz(ini_cz)  {}
  public:
    coordinata( const coordinata &ini ):  coordinata( ini.cx, ini.cy, ini.cz ) {}
    coordinata():  coordinata( 0, 0, 0 ) {}

  private:
    int cx ;
    int cy ;
    int cz ;
} ;

Nell'esempio si nota come il costruttore di copie ed il costruttore di default siano dei “Delegating Constructors”, perché affidano il compito della costruzione dell'oggetto al costruttore privato coordinata( int, int, int ).

Forwarding Constructors

[modifica | modifica wikitesto]

Capita spesso che derivando una classe si vogliano mantenere alcuni dei costruttori della classe base, nello standard C++98 questo è possibile soltanto ridefinendo tutti i costruttori utilizzati. La nuova proposta inserita nel core del linguaggio C++ è tanto semplice quanto utile.

class Base
{
  public:
    Base( int    ) ;
    Base( void   ) ;
    Base( double ) ;

    void f( int    ) ;
    void f( void   ) ;
    void f( double ) ;
} ;

class Derived: public Base
{
  public:
    using Base::f ;  // Eleva gli overload della funzione 'f'
                     // allo scope della classe derivata.
    void f( char ) ;  // Aggiunge un nuovo overload alla funzione 'f'.
    void f( int  ) ;  // Utilizza questo overload
                      // invece di 'Base::f( int )'.

    using Base::Base ;  // Sintassi proposta per elevare i costruttori
                        // della base allo scope della classe derivata.
    Derived( char ) ;  // Aggiunge un nuovo costruttore.
    Derived( int  ) ;  // Utilizza questo costruttore
                       // invece di 'Base::Base( int )'.
} ;

In pratica si tratta solo di estendere l'utilizzo di using, già utilizzabile per gli oggetti membro e per le funzioni membro, anche ai costruttori.

Diagnosi e dichiarazioni a tempo di compilazione

[modifica | modifica wikitesto]

Asserzioni statiche

[modifica | modifica wikitesto]

Le asserzioni sono espressioni booleane che esprimono una proprietà di un oggetto. Lo standard C++98 fornisce due possibilità per testare le asserzioni, la macro ASSERT e la direttiva di preprocessore #error. Però nessuna di queste è adatta per essere utilizzata nei template: la macro testa l'asserzione a tempo di esecuzione, mentre la direttiva di preprocessore testa l'asserzione a tempo di compilazione, ma prima che il template venga istanziato.

La nuova utility prevede l'introduzione di un nuovo metodo per testare le asserzioni a tempo di compilazione, mediante la nuova parola chiave static_assert. La dichiarazione assume la forma seguente:

static_assert( ''espressione-costante'', ''messaggio-di-errore'' ) ;

Ecco alcuni esempi di come la static_assert possa essere utilizzata:

static_assert( 3.14 < PGRECO && PGRECO < 3.15, "PGRECO è impreciso!" ) ;

template< class T >
struct Check
{
  static_assert( sizeof(int) <= sizeof(T), "T non è abbastanza grande!" ) ;
} ;

Quando l'espressione costante viene valutata false il compilatore genera un messaggio di errore. Il primo esempio rappresenta un'alternativa alla direttiva di preprocessore #error, nel secondo esempio invece l'asserzione viene valutata per ogni istanziazione della classe template Check.

Espressioni Lambda

[modifica | modifica wikitesto]

Le espressioni Lambda sono delle funzioni senza nome definite al momento della chiamata. Per comprendere il vantaggio di utilizzare le espressioni Lambda, immaginiamo una semplice funzione che scandisce un vettore, applicando ad ogni elemento un'operazione:

void manipola( int *inizio, int *fine )
{
  while( inizio <= fine )
  {
    *(inizio++) = 0 ;
  }
}
int vettore[10] ;
manipola( &vettore[0], &vettore[9] ) ;

Volendo utilizzare la stessa funzione con tipi diversi possiamo trasformare la funzione in una funzione template:

template< class T>
void manipola( T *inizio, T *fine )
{
  while( inizio <= fine )
  {
    *(inizio++) = 0 ;
  }
}
long double vettore[10] ;
manipola( &vettore[0], &vettore[9] ) ;

Se invece volessimo che la funzione, a seconda delle necessità, eseguisse operazioni diverse, potremmo utilizzare una enumerazione:

enum manipolazione
{
  AZZERA,
  SETTA ,
} ;
template< class T>
void manipola( T *inizio, T *fine, manipolazione mode )
{
  while( inizio <= fine )
  {
    *(inizio++) = mode == AZZERA ? 0 : 1 ;
  }
}
int vettore[10] ;
manipola( &vettore[0], &vettore[9], AZZERA ) ;

Oppure se volessimo un codice più elegante potremmo utilizzare i puntatori a funzione:

template< class T>
void manipola( T *inizio, T *fine, void (*mode)(T *) )
{
  while( inizio <= fine )
  {
    mode(inizio++) ;  // Oppure in linguaggio C: '(*mode)(inizio++) ;'.
  }
}
template< class T> void azzera( T *val )  { *val = 0 ; }
template< class T> void setta ( T *val )  { *val = 1 ; }

int vettore[10] ;
manipola( &vettore[0], &vettore[9], azzera<int> ) ;

Alcuni ritengono che queste soluzioni siano troppe verbose, allora si è pensato di introdurre le “espressioni Lambda”. Se vogliamo resettare tutto il vettore scriveremo semplicemente:

int vettore[10] ;
for_each( &vettore[0], &vettore[9], [](int & elem){ elem = 0; } ) ;

Il segmento [](int & x){ x = 0; } è l'espressione lambda che assegna 0 ad ogni elemento del vettore.

Un'espressione lambda presenta la seguente sintassi:

  • Sezione 'capture': fra le parentesi quadre è possibile specificare variabili (o riferimenti a variabili) esterne alla lambda di cui tener conto anche all'interno della stessa
  • Lista dei parametri che la funzione deve ricevere
  • Svolgimento della funzione. Il tipo del valore di ritorno è dedotto dallo svolgimento, o impostato a void se non vi è alcun return.

Tornando all'esempio precedente, è possibile eseguire anche altre operazioni per ogni elemento del vettore, ad esempio visualizzare gli elementi:

for_each( &vettore[0], &vettore[9], [](int elem){ cout << elem << ' '; } ) ;

Le variabili 'catturate' fra le quadre possono essere utilizzate all'interno delle espressioni Lambda. Nell'esempio successivo si cattura un riferimento a n e si inizializza l'elemento n-esimo al valore di n:

int n = 0 ;
for_each( &vettore[0], &vettore[9], [&n](int & x) { x = n++;} ) ;

Un'espressione lambda può avere un qualsiasi numero di argomenti. Ad esempio per ordinare un vettore in modo crescente potremo scrivere:

sort( &vettore[0], &vettore[9], [](int a, int b){ return a < b; } ) ;

La versione presentata non è la versione definitiva delle funzioni lambda: quanto esposto copre solo in parte l'argomento delle espressioni lambda, ulteriormente migliorate nelle revisioni dello standard C++14 e C++17.

Deduzione del tipo: auto e decltype

[modifica | modifica wikitesto]

Spesso la dichiarazione di una variabile non è molto agevole, soprattutto quando si tratta di tipi definiti all'interno di particolari template; prendiamo ad esempio la seguente dichiarazione:

 primo< secondo<int>, terzo<float>, quarto< bool, 128 > > ret = prima.f(val) ;

Sebbene a volte sia necessario esplicitare per intero il tipo dell'oggetto che andiamo a creare, in questo caso probabilmente è superfluo, perché può essere ricavato dal tipo di ritorno della funzione.

Utilizzando la parola chiave auto informiamo il compilatore che il tipo della variabile sarà uguale a quello del secondo membro dell'espressione:

auto ret = prima.f(val) ;
auto pgreco = 3.1415926535897932384626433832795028841L ;  // long double pgreco ;

Spesso è necessario associare il tipo di una variabile a quello di un'altra, tuttavia il programmatore è obbligato a dichiarare esplicitamente il tipo di ogni variabile che dichiara. Se il problema è l'eccessiva complessità della dichiarazione, allora conviene utilizzare il typedef, ma se l'obiettivo è proprio quello di utilizzare lo stesso tipo di una variabile, qualunque esso sia, è possibile utilizzare la nuova parola chiave decltype:

int indipendente ;
decltype(indipendente) dipendente ;  // int dipendente ;

Il comitato ritiene che il codice scritto utilizzando auto e decltype sia più ordinato e più mantenibile.

Utilità per i Template

[modifica | modifica wikitesto]

Molti degli sforzi dei ricercatori sono puntati verso il miglioramento della programmazione generica, questo perché è diventata molto popolare e viene utilizzata per esprimere classi sempre più articolate, tanto da mettere in difficoltà i mezzi dell'attuale C++. È necessario quindi che il codice generico possa esprimere soluzioni di maggiore complessità, questo senza sacrificare le caratteristiche che lo hanno reso tanto diffuso: i template sono infatti facili da scrivere, leggere ed impiegare.

Alias di template con using

[modifica | modifica wikitesto]

Nelle versioni di C++ precedenti l'undicesima è possibile utilizzare alias di template solo se tutta la lista dei parametri è definita mediante il typedef, né è possibile creare un alias se ci sono dei parametri indefiniti. Sebbene in alcuni casi sia possibile risolvere il problema utilizzando dei parametri di default, in altri l'unica scorciatoia adottabile, che non risolve completamente il problema, è quella di creare un nuovo template che inglobi il template originario. Nell'esempio che segue, infatti, i due tipi dichiarati dalle diverse tecniche di aliasing (generica_ifc_1 e generica_ifc_2) non sono riconosciuti compatibili dal compilatore.

template< class prima, class seconda, class terza > class generica{} ;
typedef generica< int, float, char > generica_ifc_1 ;

template< class seconda >class generica_iXc: generica< int, seconda, char > {} ;
typedef generica_iXc<float> generica_ifc_2 ;

Il problema è importante perché l'aliasing permette un utilizzo più flessibile delle librerie template e quindi anche della STL (Standard Template Library), ad esempio in situazioni come la seguente, sarebbe più comodo avere un alias in cui basti ripetere int una sola volta.

MyVector<int, MyAlloc<int> > vettore ;

A partire dallo standard C++11 è prevista la possibilità di dichiarare alias di template utilizzando una nuova sintassi, che appunto consenta, per esempio, di scrivere:

template< class T > using Vector = MyVector< T, MyAlloc<T> > ;
Vector<int> int_vettore ;

Come già permesso per i typedef, questa nuova sintassi potrà essere utilizzata anche all'interno di una classe per creare un alias membro (che quindi potrà essere public, protected o private).

Template variadici

[modifica | modifica wikitesto]
Lo stesso argomento in dettaglio: Template variadico.

Con il nuovo standard sarà introdotta la possibilità dichiarare template con numero arbitrario di parametri. L'impatto più importante sarà senza dubbio quello di poter estendere tutti i controlli, normalmente applicati ai parametri delle normali funzioni durante la compilazione, anche a quelle funzioni con numero arbitrario di parametri.

Per indicare l'elenco dei parametri di lunghezza variabile si utilizza l'operatore “...” che può eventualmente essere seguito dall'identificatore della lista dei parametri:

template<class T, class ...pack> class stock ;
typedef stock<int, float, double, string, 5, vector> type;

template<typename ...Arg > 
void print_template_args( const Arg& arg... ) ;
print_template_args('a', 17, 42.0, "Ciao") ;

Sull'identificatore della lista dei parametri può essere applicata una sola operazione di disassemblaggio attraverso l'operatore “...”:

template<typename ... Arg > 
void stampa_stringa(const string& s, const Arg&... arg) {
  printf  ( ("Debug: " + s).c_str(), arg... ) ;
  my_print( ("Debug: " + s).c_str(), arg... ) ;
}
stampa_stringa( "Eccoli: %d, %f, %c%s\n", 15, 0., 'f', "inito" ) ;

template<typename Primo, typename ...Secondi>
struct tuple
{
  Primo                uno ;
  generica<Secondi...> due ;
} ;
tuple<float, int, string > var ;

Nel penultimo esempio si è utilizzato la printf come esempio di variadic function, ma possono essere utilizzate anche funzioni con una normale lista di parametri, in questo caso il compilatore utilizza l'overload di my_print corrispondente all'elenco dei parametri inseriti.

Il compilatore, nell'ultimo esempio, espanderà i parametri Secondi all'interno della istanziazione della classe generica, se non esiste un overload del template che accetti due parametri, il codice è malformato e la compilazione da esito negativo. Da notare che il compilatore cercherà di istanziare generica anche se l'elenco dei parametri è vuoto, e quindi cercherà un overload del template generica senza parametri.

Di seguito, un esempio di template utilizzabile per calcolare il tempo di esecuzione di qualunque funzione che non restituisca void:

template<
    typename Function, // Tipo della funzione da eseguire
    typename ...Args   // Tipo degli argomenti della funzione
> 
std::pair< // Tipo restituito: un 'pair' (paio) che contiene il risultato della funzione e il tempo di esecuzione
    typename std::result_of<Function(Args...)>::type, // Tipo restituito dalla funzione  
    std::chrono::nanoseconds // Tipo dell'unità di misura di tempo (nanosecondi) 
> 
fun_time(Function &&f, Args &&...args) { 
    auto t0  = std::chrono::high_resolution_clock::now(); // Punto temporale prima di eseguire la funzione 'f'
    auto res = f(args...); // Esecuzione della funzione 
    auto t1  = std::chrono::high_resolution_clock::now(); // Punto temporale dopo aver eseguito la funzione 'f' 
    auto dT  = std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0); // Nanosecondi dati dalla differenza tra 't1' e 't0'
    return std::make_pair(res, dT); // Restituzione del risultato di 'f' e del tempo di esecuzione
}

Spesso i parametri che utilizziamo per un template devono possedere delle determinate caratteristiche, altrimenti rischiamo errori di compilazione, oppure (questo è grave) istanziazioni prive di logica. Nello standard attuale la definizione è l'unico controllo che assumiamo sui parametri di un template, mentre sarebbe più opportuno controllare le sue istanziazioni indipendentemente dalla definizione del template, e viceversa. Per questo fu proposta l'introduzione dei concept: un sistema per migliorare la diagnostica degli errori (quindi le prestazioni offerte dai template), senza perdere capacità espressiva. Tuttavia il gruppo ISO responsabile dello sviluppo dello standard decise di non introdurre i concept poiché ritenuti ancora immaturi rispetto alle scadenze che il gruppo stesso si era prefissato per la standardizzazione della nuova versione del linguaggio; è tuttavia probabile che tale funzionalità verrà proposta per i lavori dello standard successivo[9].

Osserviamo la funzione template seguente che esegue un ordinamento mediante l'algoritmo di quick sort:

 template< class PUNT >
 void quick_sort( PUNT primo, PUNT ultimo )
 {
   if( primo < ultimo )
   {
     typename result_of<PUNT::operator*()> pivot = *primo ;
 
     PUNT pDestro   = ultimo ;
     PUNT pSinistro = primo  ;
     ++ultimo ; --primo ;
     while( pSinistro < pDestro )
     {
       while( *(--pDestro  ) > pivot ) ;
       while( *(++pSinistro) < pivot ) ;
       if( *pSinistro < *pDestro )
       {
         typename result_of<PUNT::operator*()> temp ;
         temp       = *pSinistro ;
         *pSinistro = *pDestro   ;
         *pDestro   = temp       ;
       }
     }
     quick_sort( primo    , pDestro ) ;
     quick_sort( ++pDestro, ultimo  ) ;
   }
 }

È evidente che i tipi PUNT e result_of<PUNT::operator*()> utilizzati in questa funzione devono possedere delle funzioni membro senza le quali la compilazione sarebbe impossibile.

La definizione di un Concept è composta da una lista di parametri, esplicitati come per la dichiarazione di un template, ed il corpo del Concept, una sequenza di semplici dichiarazioni che dipendono dai parametri della dichiarazione.

 concept< class PUNT > quick_restriction
 {
   PUNT a ;  // Esista un costruttore di default per PUNT.
   PUNT b = a ;  // Esista un costruttore di copie per PUNT.
 
   ++a ;  // PUNT possieda un operatore di incremento prefisso.
   --a ;  // PUNT possiede un operatore di decremento prefisso.
 
   bool bl = a < b ;  // PUNT supporti l'operatore di confronto '<'.
 
   typename result_of<PUNT::operator*()> e = *a ;  // L'elemento del vettore abbia
                                                   // un costruttore di copie.
 
   e = *a ;  // L'elemento del vettore sia fornito dell'operatore di assegnamento. 
 
   bl = e < *a ;  // Siano supportati gli operatori di
   bl = e > *a ;  // confronto < e > per l'elemento del vettore.
 } ;

Il Concept viene introdotto nella definizione del template mediante la keyword where:

 template< class PUNT > where quick_restriction<PUNT>
 void quick_sort( PUNT primo, PUNT ultimo )  { /*...*/ }

Con la stessa sintassi si può utilizzare la where anche all'interno della definizione di un concept per esprimere ulteriori restrizioni sulla combinazione di parametri.

Infine, all'interno della definizione della classe template, sarà possibile utilizzare i type traits (una nuova utility della libreria standard, vedi il paragrafo relativo) per pilotare la compilazione in funzione delle caratteristiche dei parametri assegnati.

Parentesi angolari

[modifica | modifica wikitesto]

Con l'introduzione della programmazione generica attraverso i template fu necessario introdurre un nuovo tipo di parentesi. Oltre alle parentesi tonde, quadre e graffe, sono state introdotte le parentesi angolari. Il compilatore, dal momento in cui sono state introdotte le nuove parentesi, deve discriminare quando i caratteri < e > sono utilizzati nelle espressioni logiche, quando invece sono utilizzati come operatori di inserimento (<<) ed estrazione (>>), oppure, per l'appunto, quando sono utilizzati come parentesi angolari; questo ha fatto nascere ovviamente alcune ambiguità:

 typedef std::vector<std::vector<int> > Table ;  // Ok.
 typedef std::vector<std::vector<bool>> Flags ;  // Errore!
 
 void func( List<B>= default_val ) ;  // Errore!
 void func( List<List<B>>= default_val ) ;  // Errore!
 
 template< bool I > class X  {} ;
 X< 1>2 > x1 ;  // Errore!

L'ultimo esempio è un po' astruso ma verificabile. Sono possibili due istanziazioni della classe X: una con I==false ed una con I==true; il codice definisce un'istanza di X in base al valore della relazione costante 1>2 (uguale a false ovviamente), ma il compilatore interpreta > come una parentesi angolare destra ed istanzia la X<true>, quindi il resto del codice diventa privo di senso e la compilazione fallisce.

La soluzione al problema è abbastanza semplice: nel prossimo standard il compilatore, dopo l'apertura di una parentesi angolare sinistra, dovrà interpretare la sequenza di caratteri >> come una doppia parentesi angolare destra, senza lasciarsi trarre in inganno dalle sequenze >= e >>=. Mentre per l'ultimo esempio il programmatore dovrà delimitare dalle parentesi tonde il contenuto della relazione costante.

 X<(1>2)> x1 ;  // Ok.

In questo modo, dopo la parentesi tonda sinistra e fino alla parentesi tonda destra, il compilatore non riconosce più i caratteri <> come parentesi angolari.

Altre Utility

[modifica | modifica wikitesto]

typedef opaco

[modifica | modifica wikitesto]

Questa caratteristica è stata giudicata come non pronta per il C++09, ma da poter riproporre nel futuro dal comitato.

Capita spesso nei programmi di definire più variabili dello stesso tipo, ma che rappresentano grandezze assolutamente non compatibili tra loro. L'esempio più comune è quello di un sistema di coordinate espresso tramite valori double:

 struct punto
 {
   punto( double x, double y, double z ) ;
   //...
 } ;

In programmi di una certa complessità si avverte la necessità di apportare una distinzione netta tra i tre tipi, soprattutto per ragioni di chiarezza e di mantenibilità del codice:

 typedef double Cx, Cy, Cz ;
 struct punto
 {
   punto( Cx x, Cy y, Cz z ) ;
   //...
 } ;

Questa soluzione in apparenza sembra migliore della precedente ma, in pratica, il compilatore non effettua ancora nessun controllo sui parametri, i quali restano mutuamente sostituibili.

L'unica soluzione possibile è quella di definire tre nuove classi, e programmare tutti gli overload di ogni operatore ammesso. Questa soluzione è ideale, soprattutto se applicata in un programma di grande complessità, perché permette di ottenere un controllo totale sui nuovi tipi; lo sforzo per lo sviluppo dei nuovi oggetti sarebbe comunque poca cosa rispetto ai benefici ottenuti.

Tuttavia se il programmatore non necessita di un controllo così stringente, sarebbe molto più semplice introdurre un nuovo typedef che non crei un semplice alias, ma un nuovo oggetto. Per questo motivo è in fase di sviluppo il cosiddetto “typedef opaco”, mediante il quale sarà possibile creare un nuovo tipo che riceve in eredità tutte le caratteristiche del precedente, senza esserne il sostituto.

Saranno introdotti due tipi di typedef opachi: il typedef public ed il typedef private. Es:

 typedef public origine nuovo ;

Il primo permetterà ancora la conversione da origine a nuovo (ovviamente non implicita, altrimenti saremmo ancora al punto di partenza) e permetterà ancora una conversione implicita da nuovo a origine, però il nuovo tipo sarà ben distinto dal precedente.

 typedef public double Cx, Cy, Cz ;
 Cx x = 10 ;
 Cy y ;
 y = x ;  // Errore!
 y = static_cast<Cy>(x) ;  // Ok.

Il secondo è la forma più restrittiva del concetto opaco: per il nuovo tipo, se si vuole permettere delle conversioni con il tipo originario e viceversa, bisognerà definire delle funzioni ad hoc.

 typedef private double Cx, Cy, Cz ;
 Cx x = 10 ;
 Cy y ;
 y = x ;  // Errore!
 y = static_cast<Cy>(x) ;  // Errore!

La tecnica dei “typedef opachi” non è un metodo per utilizzare qualsiasi classe come se fosse una classe template. I nuovi tipi definiti in questa maniera restano degli alias degli originali seppur con qualche restrizioni.

long long int

[modifica | modifica wikitesto]

Fin dai tempi del C c'è sempre stato un tipo integrale di troppo nel Core del linguaggio, in genere int era composto dallo stesso numero di byte della macchina sistema. Lo standard prevedeva per l'int 16 o 32 bit condannando lo short int (abbreviato short) oppure il long int (abbreviato long) ad un ruolo subalterno.

Nei nuovi sistemi a 64 bit i produttori di compilatori hanno messo fine a questa ridondanza di definizione assegnando definitivamente:

  • 16 bit -> short int
  • 32 bit -> int
  • 64 bit -> long int

Tuttavia, nei sistemi a 32 bit, resta radicata l'abitudine dei produttori di compilatori ad utilizzare long long int come numero a 64 bit. Il comitato del C++ si è sempre dimostrato riluttante a standardizzare nuovi tipi fondamentali che non siano anche stati adottati dal comitato del C (che gode di assoluta indipendenza da quello del C++). Ora che la dicitura è diventata uno “standard di fatto”, questo vincolo sembra possa essere finalmente superato. Il comitato C++ ha approvato long long int tra i tipi fondamentali (compreso unsigned long long int).

D'altra parte, in futuro, questa dicitura potrebbe essere ancora utilizzata in sistemi basati su processori con registri a 16 bit per indicare, appunto, numeri a 128 bit.

Puntatore nullo

[modifica | modifica wikitesto]

Nello standard pre-11 allo “0” spettava il doppio ruolo di costante intera e di puntatore nullo (soluzione adottata fin dagli albori del C nel 1972).

Per anni i programmatori hanno risposto a questa possibile ambiguità utilizzando la costante “NULL”, al posto dello “0”, anche per rendere il codice più comprensibile. Dal 2011 è stata inclusa una nuova parola chiave (nullptr) riservata esclusivamente per indicare il puntatore nullo.

Il nullptr non può essere assegnato ad un tipo intero, né confrontato con esso, mentre può essere confrontato ed assegnato a qualsiasi puntatore

Resta ovviamente possibile assegnare ad un puntatore la costante “0” per ragioni di compatibilità, tuttavia l'utilizzo di 0 e NULL è sconsigliato in ogni codice che non richieda retrocompatibilità, poiché potrebbe portare ad errori. Ad esempio, consideriamo il codice seguente:

#include <iostream>
void foo(const char *)
{
    std::cout << "Puntatore" << std::endl;
}
void foo(int)
{
    std::cout << "Intero" << std::endl;
}

int main()
{
    foo(NULL);      //Probabilmente stamperà "Intero", o genererà un errore di compilazione
    foo(nullptr);   //Questa chiamata invece senza dubbio stamperà "Puntatore"
}

La “enum class”

[modifica | modifica wikitesto]

Nello standard C++-98 le enum non sono tipizzate come una classe, inoltre il tipo degli elementi è int e non se ne possono utilizzare altri. Possono essere utilizzati senza dichiararne lo scope e quindi non è possibile dichiarare in due distinte enumerazioni, due elementi con lo stesso nome. Inoltre gli elementi di una enumerazione possono essere convertiti implicitamente ad int e questo è da sempre causa di innumerevoli errori per il programma.

Se il programmatore vuole utilizzare dei tipi più sicuri, è obbligato a sviluppare delle classi come la seguente:

 enum costanti
 {
   //...
 } ;
 class valore
 {
   public:
     valore( void )  {}  // Costr. di default.
     valore( const valore   &ini ):  pri_val(ini.pri_val)  {}  // Costr. di copie.
     valore(       costanti  ini ):  pri_val(ini        )  {}
 
     void operator=( valore   ini )  { pri_val = ini.pri_val ; }
     void operator=( costanti ini )  { pri_val = ini         ; }
 
     bool operator==( valore   val ) const  { return pri_val == val.pri_val ; }
     bool operator==( costanti val ) const  { return pri_val == val         ; }
     bool operator!=( valore   val ) const  { return pri_val != val.pri_val ; }
     bool operator!=( costanti val ) const  { return pri_val != val         ; }
 
   protected:
     short pri_val ;
 } ;

Le uniche azioni permesse tra oggetti di tipo valore sono l'assegnamento ed il confronto, questo dovrebbe mettere al riparo da possibili errori, a patto di convertire ogni valore della enum costanti utilizzato all'interno del codice con un oggetto valore. (Ad essere pignoli questa classe si presta facilmente ad essere trasformata in un template, per essere istanziata a partire da diverse enumerazioni).

A partire dallo standard C++11 non sono più necessarie dichiarazioni verbose come quella precedente per utilizzare una semplice enum ed avere l'appoggio del compilatore per la ricerca di possibili errori sintattici. Sono infatti state introdotte due espressioni riguardanti gli enum:

  • Per gli enum classici si potrà indicare esplicitamente il tipo degli elementi della enumerazione. Il tipo dovrà essere un "integral type" (intero o carattere), con o senza segno.
 enum E: unsigned long
 {
   E1   =          1 ,
   E2   =          2 ,
   Ebig = xFFFFFFF0ul,
 } ;

Resta invariata la sintassi per indicare gli elementi della enum.

 E e1 = E1    ;  // OK.
 E e1 = E::E1 ;  // OK.
  • Inoltre è stato introdotto un nuovo tipo di enumerazione: la enum class, fortemente tipizzata: non sono supportati i cast impliciti e, per indicare ogni elemento, è sempre necessario indicarne lo scope. Anche per la enum è possibile indicare esplicitamente il tipo degli elementi; se omesso sarà utilizzato int.
 enum class E: short
 {
   E1      ,
   E2 = 10 ,
   E3      ,
 } ;
 enum class N  // Equivale ad 'enum class N: int'.
 {
   N1,
 } ;
 E e1 = E1 ;     // Errore: è necessario specificare lo scope 'E'.
 E e2 = E::E2 ;  // OK.
 N n1 = N::N1 ;  // OK.
 bool b1 = e2 >= 100 ;  // Errore: la enum class non può essere convertita
                        // ad int per il confronto, né int può essere
                        // convertito nella enum class.

A partire dal C++11 è stato introdotto il ranged for, un metodo più veloce per iterare attraverso gli elementi di un array o di un container. Affinché un oggetto sia iterabile mediante un range-based for è necessario che disponga di un metodo begin e un metodo end, o che esista un overload di std:begin e std::end per la classe dell'oggetto.

La sintassi di un range-based for è for ( tipo nome_elemento : nome_container) { espressioni... }; una caratteristica molto comoda di questo ciclo è che l'elemento dichiarato è di tipo *container::iterator, pertanto è possibile lavorare direttamente sull'elemento invece che sull'iteratore.

std::vector<std::string> vs{"aa", "bb", "cc"};
for (auto str : vs)     //str dedotto di tipo std::string
    std::cout << str << ' ';

std::vector<int> v{1, 2, 3, 4, 5};
for (auto & num : v)    //auto deduce int, pertanto num è di tipo int &
    num *= num;

Estensioni alla libreria standard C++

[modifica | modifica wikitesto]

Dalla Standard library del C++0x arriveranno le novità più ardite, anche se, in realtà, quasi tutte le nuove librerie non necessitano dell'aggiornamento del core e potrebbero funzionare anche sullo standard C++ corrente.

La maggior parte delle librerie che saranno introdotte sono definite nel documento “Technical Report on C++ Library Extensions” (chiamato anche TR1), la cui stesura definitiva risale al 2005. Queste librerie sono già state adottate da alcuni compilatori e possono essere richiamate mediante il “namespace std::tr1”.

È in preparazione un secondo technical report (TR2) ma sicuramente verrà completato dopo la standardizzazione del C++0x. Per questa ragione il paragrafo corrente referenzia esclusivamente alcune delle librerie più significative introdotte attraverso il TR1.

Le tuple sono collezioni di dimensioni prestabilite composte da oggetti di tipo eterogeneo. Gli elementi di una tupla possono essere un qualsiasi tipo di oggetto.

Questa utilità viene implementata nell'header <tuple> e beneficia di alcune estensioni del linguaggio C++, come:

  • template con lista di argomenti di lunghezza variabile,
  • riferimenti a riferimenti,
  • argomenti di default per le funzioni template (disponibili solo per le classi).

Ecco la definizione di tupla nell'header <tuple>:

 template< class T1 = unspecified,
           class T2 = unspecified,
           ...,
           class TM = unspecified > class tuple ;

Esempio di definizione ed uso di una tupla:

 typedef tuple< int, double, long &, const char * > tupla_di_prova ;
 long lungo = 12 ;
 tupla_di_prova prova( 18, 6.5, lungo, "Ciao!" ) ;
 lungo = get<0>(prova) ;  // Assegna a 'lungo' il valore 18.
 get<3>(prova) = "Bello!" ;  // Modifica il quarto elemento della tupla.
 
 //È anche possibile generare una tupla sfruttando la deduzione del tipo (auto)
 // e la funzione std::make_tuple
 auto altra_tupla = std::make_tuple(42, "cavallo", lungo, 'n');

È possibile creare la tupla prova senza definirne il suo contenuto, ma questo solo se tutti gli oggetti della tupla possiedono il costruttore di default; inoltre si può assegnare una tupla ad un'altra: se le tuple sono dello stesso tipo, sarà necessario che tutti gli oggetti possiedano il costruttore di copie; se gli oggetti non corrispondono, sarà necessario che quelli del 2º membro siano convertibili a quelli del 1º, oppure che i tipi del 1º membro abbiano un costruttore adeguato:

 typedef tuple< int , double, string       > tupla_1 t1 ;
 typedef tuple< char, short , const char * > tupla_2 t2( 'X', 2, "Hola!" ) ;
 t1 = t2 ;  // Ok, i primi 2 possono essere convertiti,
            // il 3º accetta come costruttore un 'const char *'.

Sono anche disponibili gli operatori di confronto (tra tuple con uguale numero di elementi e fra tuple da due elementi e std::pair) ed inoltre sono disponibili due espressioni per controllare le caratteristiche delle tuple (solo a tempo di compilazione):

  • tuple_size<T>::value Ritorna il valore del numero di elementi della tupla di tipo T.
  • tuple_element<I, T>::type Ritorna il tipo dell'elemento numero I della tupla di tipo T.

Tabelle di Hash

[modifica | modifica wikitesto]

L'inserimento nella libreria standard del C++ delle tabelle di Hash (i contenitori associativi non ordinati) è stata una delle richieste più frequenti. Sebbene queste soluzioni diano rendimenti inferiori rispetto agli alberi bilanciati se utilizzate nel caso peggiore (ossia in presenza di molte collisioni), le loro performance sono migliori in molte applicazioni reali.

La gestione delle collisioni viene amministrata soltanto mediante linear chaining. Questo perché il comitato non ha ritenuto opportuno standardizzare soluzioni di open addressing che presentano parecchi problemi intrinseci (soprattutto quando è ammessa la cancellazione di elementi). A causa delle possibili omonimie con librerie non standard, che nel frattempo hanno colmato la mancanza di una libreria standard per le tabelle di Hash, è stato utilizzato il prefisso “unordered” invece di “hash”.

La nuova utility prevede 4 tipi di tabelle di Hash, differenti a seconda che accettino o no più elementi con la stessa chiave (chiavi equivalenti o chiavi uniche) ed a seconda che associno o no un valore arbitrario alla chiave.

Tipo di tabella di hash Mappatura arbitraria Chiavi equivalenti
unordered_set
unordered_multiset X
unordered_map X
unordered_multimap X X

Le nuove classi sono modellate sul concetto di container, pertanto ne implementano tutte le funzioni, comprese quelle necessarie per accedere agli elementi come: insert, erase, begin, end.

Per utilizzare le tabelle di hash è necessario includere gli headers <unordered_set> e <unordered_map> secondo necessità.

Espressioni regolari

[modifica | modifica wikitesto]

A partire dal C++11 la libreria standard ha incluso una propria libreria per le gestione delle espressioni regolari. La nuova libreria, definita nel nuovo header <regex>, consiste in una coppia di nuove classi:

  • le espressioni regolari sono rappresentate dalle istanze della classe template basic_regex;
  • le corrispondenze sono rappresentate dalle istanze della classe template match_results.

Per la ricerca si utilizza la funzione regex_search, mentre per la ricerca e la sostituzione si utilizza regex_replace, che fornisce una nuova stringa corretta come risultato. Gli algoritmi regex_search e regex_replace ricevono come input una espressione regolare ed una stringa di caratteri e scrivono le corrispondenze trovate nella struttura match_results.

Esempio di utilizzo di match_results:

 const char *reg_espr = "[ ,.\\t\\n;:]" ;  // Elenco dei caratteri separatori.
 // NOTA: l'apparato delle espressioni regolari considera il [[backslash]] come il
 // compilatore C++, quindi, ad esempio, il carattere '\n' va indicato con "\\n".
 
 regex rgx(reg_esp) ;  // 'regex' è un'istanza della classe template
                       // 'basic_regex' con argomento di tipo 'char'.
 cmatch match ;  // 'cmatch' è un'istanza (predefinita) del template
                 // 'match_results' con argomento di tipo 'const char *'.
 const char *target = "Politecnico di Torino " ;
 
 // Identifica tutte le parole di 'target' delimitate dai caratteri di 'reg_espr'.
 if( regex_search( target, match, rgx ) == true )
 {
   // Se sono presenti delle parole separate dai caratteri specificati.
 
   for( int a = 0 ; a < match.size() ; a++ )
   {
     string str( matches[a].first, matches[a].second ) ;
     cout << str << "\n" ;
   }
 }

La libreria “regex” non richiede l'alterazione di nessun header esistente e nessuna estensione del linguaggio.

Puntatori smart per usi generici

[modifica | modifica wikitesto]

La gestione dell'allocazione dinamica della memoria è sempre stata, fin dai primi computer, un punto delicato della programmazione. Molti moderni linguaggi di programmazione (tipo il Java) offrono strumenti per la gestione automatica della memoria.

I puntatori ordinari del C++ hanno molte interessanti proprietà:

  • è possibile copiarli,
  • assegnarli,
  • utilizzarne il loro valore,
  • utilizzare il void * come puntatore generico,
  • convertirli ad una delle classi base attraverso un cast statico,
  • convertirli ad una delle classi derivate attraverso un cast dinamico.

Mentre i principali difetti dei puntatori ordinari del C++ sono:

  • la gestione manuale obbligata per gli oggetti allocati dinamicamente,
  • possono riferirsi ad un indirizzo non valido o non allocato della memoria.

I nuovi smart pointer mantengono le caratteristiche di forza dei puntatori ordinari eliminando le loro debolezze.

Utilizzando i puntatori shared_ptr la proprietà dell'oggetto viene ripartita egualmente a tutte le copie, all'ultima istanza rimasta viene delegata la responsabilità di distruggere l'oggetto. Per conoscere il numero di puntatori che fanno riferimento allo stesso oggetto è possibile utilizzare la use_count, una funzione membro di shared_ptr. La funzione membro reset permette di cancellare lo smart pointer. Un puntatore resettato si dice vuoto, quindi la sua funzione use_count ritorna sempre zero. Il puntatore weak_ptr non incide sul ciclo di vita dell'oggetto puntato, questo significa che in ogni momento è possibile che il weak_ptr venga invalidato. In questo modo è permesso a qualsiasi funzione o classe di mantenere un riferimento ad un oggetto senza influenzarne il ciclo di vita, a discapito però di una maggiore difficoltà di implementazione del codice.

Esempio di utilizzo dello shared_ptr:

 int main( void )
 {
   shared_ptr<double> p_primo(new double) ;
 
   if( true )
   {
     shared_ptr<double> p_copia = p_primo ;
 
     *p_copia = 21.2 ;
 
   }  // Distruzione di 'p_copia' ma non del double allocato.
 
   return ;  // Distruzione di 'p_primo' e di conseguenza del double allocato.
 }

Un terzo smart pointer è lo unique_ptr: un puntatore tale da esser l'unico detentore dell'area di memoria che ha allocato. Non è possibile copiare uno unique_ptr : è solo possibile trasferire la proprietà da un puntatore all'altro mediante il supporto della funzione std::move().

È possibile utilizzare gli smart pointers includendo l'header <memory>; è inoltre buona norma rimpiazzare l'uso di auto_ptr, deprecato in questo sin da questo standard con i nuovi smart pointers.

Utilità estensibile per numeri casuali

[modifica | modifica wikitesto]

I computer hanno per definizione comportamento deterministico, tuttavia alcune applicazioni richiedono un comportamento non deterministico (anche se solo in apparenza) veicolato dalla generazione di numeri casuali.

La sola utilità standard presente prima del 2011 era la funzione rand, ma non è ben definita e la sua implementazione è delegata interamente ai produttori dei compilatori. Pertanto sono state introdotte nuove utilità per generare numeri casuali nell'header <random>.

I generatori di numeri casuali sono costituiti da uno stato interno, ed una funzione che elabora il risultato e porta il generatore allo stato successivo. Queste due caratteristiche costituiscono il motore del generatore. Un'altra fondamentale caratteristica è infine la distribuzione dei risultati, ossia l'intervallo e la densità della variabile aleatoria.

Attraverso il template variate_generator è possibile creare un generatore di numeri casuali selezionando il motore e la distribuzione desiderata. Si può scegliere tra i motori e le distribuzioni fornite dallo standard, oppure utilizzare mezzi propri.

  • Motori per numeri pseudocasuali

Nella nuova libreria verranno introdotti alcuni motori per la generazione di numeri pseudocasuali. Questi sono tutti dei template, in questo modo l'utente può personalizzarli come preferisce. Lo stato interno dei motori pseudocasuali è determinato attraverso un seme (generalmente un insieme di variabili). L'apparente casualità è dovuta solo dalla limitata percezione dell'utente.

classe template int/float qualità velocità dimensione stato*
linear_congruential int bassa media 1
substract_with_carry entrambi media veloce 25
mersenne_twister int buona veloce 624

* Moltiplicare il valore per la dimensione in byte del tipo utilizzato.

Le prestazioni di questi motori possono essere incrementate utilizzando la classe template discard_block, oppure possono essere combinate utilizzando la classe template xor_combine. Per comodità nell'header <random> sono definite anche alcune istanze standard di motori; un esempio è la classe mt19937 istanziata su base mersenne_twister:

 typedef mersenne_twister< ''def.dall'implementazione'', 32, 624, 397, 31, 0x9908b0df,
                           11, 7, 0x9d2c5680, 15, 0xefc60000, 18 >
         mt19937 ;
  • Motore per numeri non deterministici

Attraverso la classe random_device è possibile generare numeri non deterministici di tipo unsigned int. La sua implementazione richiederà l'utilizzo di un dispositivo il cui stato sia indipendente dal sistema che ospita l'applicazione (ad esempio da un contatore esterno non sincronizzato, oppure un trasduttore particolare) e richiederà anche l'impiego di un tradizionale motore pseudocasuale per, come si usa dire, “temprare il risultato”.

  • Distribuzioni dei numeri casuali

La libreria definisce parecchi tipi di distribuzioni, dalle distribuzioni uniformi a quelle definite dalla teoria della probabilità: uniform_int, bernoulli_distribution, geometric_distribution, poisson_distribution, binomial_distribution, uniform_real, exponential_distribution, normal_distribution e gamma_distribution. Ovviamente l'utente è libero di instanziare come preferisce le distribuzioni standard oppure di utilizzare una sua distribuzione compatibile.

Ecco un semplice esempio di implementazione:

 uniform_int<int> distribuzione( 0, 99 ) ;
 mt19937 motore ;
 variate_generator<mt19937, uniform_int<int>> generatore( motore, distribuzione );
 int casuale = generatore() ;  // Assegna un valore casuale tra 0 e 99.

Funzioni matematiche speciali

[modifica | modifica wikitesto]

L'header <math> definisce alcune funzioni matematiche abbastanza comuni:

  • trigonometriche: sin, cos, tan, asin, acos, atan e atan2;
  • iperboliche: sinh, cosh, tanh, asinh, acosh, atanh;
  • esponenziali: exp, exp2, frexp, ldexp, expm1;
  • logaritmiche: log10, log2, logb, ilogb, log1p;
  • potenze: pow, sqrt, cbrt, hypot;
  • speciali: erf, erfc, tgamma, lgamma.

La proposta era di aggiungere nuove funzioni alla categoria ‘speciali' per colmare parecchie lacune che costringono ad utilizzare librerie non standardizzate, tuttavia tale cambiamento non è stato approvato per la versione finale del C++11. Chiaramente l'utilizzo di queste funzioni sarebbe stato limitato all'ambito ingegneristico ed alle discipline scientifiche.

La tabella seguente riporta le 23 funzioni approvate per lo standard C++11.

Nome della funzione Prototipo della funzione Espressione matematica
Polinomi associati di Laguerre double assoc_laguerre(unsigned n, unsigned m, double x) ;
Polinomi associati di Legendre double assoc_legendre(unsigned l, unsigned m, double x) ;
Funzione beta di Eulero double beta(double x, double y) ;
Integrale ellittico completo di prima specie double comp_ellint_1(double k) ;
Integrale ellittico completo di seconda specie double comp_ellint_2(double k) ;
Integrale ellittico completo di terza specie double comp_ellint_3(double k, double nu) ;
Funzione ipergeometrica confluente double conf_hyperg(double a, double c, double x) ;
Funzione cilindrica di Bessel modificata regolarmente double cyl_bessel_i(double nu, double x) ;
Funzione cilindrica di Bessel di prima specie double cyl_bessel_j(double nu, double x) ;
Funzione cilindrica di Bessel modificata irregolarmente double cyl_bessel_k(double nu, double x) ;
Funzione cilindrica di Neumann

Funzione cilindrica di Bessel di seconda specie

double cyl_neumann(double nu, double x) ;
Integrale ellittico incompleto di prima specie double ellint_1(double k, double phi) ;
Integrale ellittico incompleto di seconda specie double ellint_2(double k, double phi) ;
Integrale ellittico incompleto di terza specie double ellint_3(double k, double nu, double phi) ;
Integrale esponenziale double expint(double x) ;
Polinomi di Hermite double hermite(unsigned n, double x) ;
Serie ipergeometrica double hyperg(double a, double b, double c, double x) ;
Polinomi di Laguerre double laguerre(unsigned n, double x) ;
Polinomi di Legendre double legendre(unsigned l, double x) ;
Funzione zeta di Riemann double riemann_zeta(double x) ;
Funzione sferica di Bessel di prima specie double sph_bessel(unsigned n, double x) ;
Funzione sferica associata di Legendre double sph_legendre(unsigned l, unsigned m, double theta) ;
Funzione sferica di Neumann

Funzione sferica di Bessel di seconda specie

double sph_neumann(unsigned n, double x) ;

Ad ogni funzione è sufficiente aggiungere il suffisso ‘f' per ottenere la versione ‘float' ed il suffisso ‘l' per la versione ‘long double'. Es:

 float sph_neumann''f''( unsigned n, float x ) ; long double sph_neumann''l''( unsigned n, long double x ) ;

Wrapper reference

[modifica | modifica wikitesto]

Il wrapper reference viene ottenuto da un'istanza della classe template reference_wrapper nell'header <utility>, il suo utilizzo è simile al riferimento ‘&' previsto dal linguaggio C++. Per ottenere un wrapper reference da un oggetto qualsiasi si utilizza la funzione template ref (per un riferimento costante si usa cref).

Il wrapper reference è utile soprattutto per le funzioni template quando vogliamo che ottengano un riferimento ai loro parametri invece di utilizzarne una copia:

 // Questa funzione ottiene il parametro 'r' per riferimento e lo incrementerà.
 void f( int &r )  { r++ ; }
 
 // Funzione template.
 template< class F, class P > void g( F f, P t )  { f(t) ; }
 
 int main()
 {
   int i = 0 ;
   g( f, i ) ;  // Viene istanziata 'g< void ( int &r ), int >'
                // quindi 'i' non viene modificato.
   cout << i << endl ;  // Output -> 0
 
   g( f, ref(i) ) ;  // Viene istanziata 'g<void(int r),reference_wrapper<int>>'
                     // quindi 'i' sarà modificato.
   cout << i << endl ;  // Output -> 1
 }

Wrapper polimorfi per oggetti funzione

[modifica | modifica wikitesto]

I wrapper polimorfi per oggetti funzione (Polymorphic Function Object Wrapper) sono simili ai puntatori a funzione per semantica e sintassi, ma il loro utilizzo è meno vincolato e possono riferirsi indistintamente a qualsiasi funzione che possa essere chiama con argomenti compatibili con quelli del wrapper.

Attraverso l'esempio seguente è possibile comprenderne le caratteristiche:

 function<int ( int, int )> pf ;  // Creazione del wrapper tramite la classe
                                  // template 'function'.
 
 plus<int> add ;  // 'plus' è dichiarato come 'template<class T> T plus( T, T ) ;'
                  // quindi 'add' è di tipo 'int add( int x, int y )'.
 
 pf = &add ;  // L'assegnamento è corretto perché i
              // parametri ed il tipo di ritorno corrispondono.
 
 int a = pf( 1, 2 ) ;  // NOTA: se il wrapper 'pf' non è riferito a nessuna
                       // funzione viene lanciata l'[[Eccezione (informatica)|eccezione]] 'bad_function_call'.
 
 function<bool ( short, short )> pg ;
 if( pg == nullptr )  // Sempre verificata perché 'pg' non
                      // è ancora assegnata a nessuna funzione.
 {
   bool adjacent( long x, long y ) ;
   pg = &adjacent ;  // I parametri ed il valore di ritorno sono compatibili,
                     // l'assegnamento è corretto.
   struct prova
   {
     bool operator()( short x, short y ) ;
   } car ;
   pg = ref(car) ;  // 'ref' è una funzione template che ritorna il wrapper
                    // della funzione membro 'operator()' di 'car'.
 }
 pf = pg ;  // È corretto perché i parametri ed il valore di ritorno del
            // wrapper 'pg' sono compatibili con quelli del wrapper 'pf'.

La classe template function è definita all'interno dell'header <functional>.

I type traits per la metaprogrammazione

[modifica | modifica wikitesto]

La metaprogrammazione consiste nel creare un programma che crei o modifichi un altro programma (o se stesso). Questo può avvenire a tempo di compilazione oppure a tempo di esecuzione. Il comitato del C++ ha deciso di introdurre una libreria che consenta la metaprogrammazione a tempo di compilazione attraverso i template.

Ecco un ottimo esempio di quello che si può già ottenere, con lo standard attuale, attraverso la metaprogrammazione: una ricorsione di istanziazioni di template per il calcolo di una potenza.

 template< int B, int N >
 struct Pow
 {
   // Chiamata ricorsiva e ricombinazione.
   enum{ value = B*Pow< B, N-1 >::value } ;
 } ;
 template< int B > struct Pow< B, 0 >  // N == 0 condizione di terminazione.
 {
   enum{ value = 1 } ;
 } ;
 int tre_alla_quarta = Pow< 3, 4 >::value ;

Molti algoritmi possono essere utilizzati indistintamente per diversi tipi di dati, per questo motivo sono stati inseriti nello standard C++ i template, in modo da supportare la programmazione generica e rendere più compatto e gestibile il codice. Tuttavia capita spesso di imbattersi in algoritmi che necessitano di informazioni sui dati utilizzati. Queste informazioni possono essere ricavate durante l'istanziazione di una qualsiasi classe template utilizzando i type traits.

I type traits sono moltissimi e possono identificare la categoria di un oggetto e tutte le caratteristiche di una classe (o di una struttura). Sono definiti nel nuovo header <type_traits>.

Nell'esempio seguente c'è la funzione template ‘elabora' che a seconda del tipo di dati inseriti istanzierà uno dei due algoritmi proposti (funzione.do_it).

 // Primo modo di operare.
 template< bool B > struct funzione
 {
   template< class T1, class T2 > int do_it( T1 &, T2 & )  { /*...*/ }
 } ;
 // Secondo modo di operare.
 template<> struct funzione<true>
 {
   template< class T1, class T2 > int do_it()( T1 *, T2 * )  { /*...*/ }
 } ;
 
 // Istanziando 'elabora' si instanzia automaticamente il modo di operare corretto.
 template< class T1, class T2 > int elabora( T1 A, T2 B )
 {
   // Utilizza il secondo modo solo se 'T1' è un tipo intero e se 'T2' è
   // un tipo in virgola mobile, altrimenti utilizza il primo modo.
   return funzione< is_integral<T1> && is_floating_point<T2> >::do_it( A, B ) ;
 }

Attraverso i type traits, definiti nell'header <type_transform>, è possibile effettuare anche delle operazioni di trasformazioni sui tipi (lo static_cast ed il const_cast non sono sufficienti all'interno di un template).

Questo tipo di programmazione permette di ottenere un codice elegante e conciso; il vero punto debole di queste tecniche è il debugging: disagevole a tempo di compilazione e molto difficile durante l'esecuzione del programma.

Metodo Uniforme per determinare i tipi di ritorno di un oggetto funzione

[modifica | modifica wikitesto]

Determinare (a tempo di compilazione) il tipo di ritorno di una funzione oggetto template, soprattutto se dipende dai parametri della funzione stessa, non è sempre intuitivo.

Prendiamo in esempio il codice seguente:

 struct chiara
 {
   int    operator()( int    ) ;  // Il tipo del parametro è
   double operator()( double ) ;  // uguale al tipo ritorno.
 } ;
 
 template< class Obj > class calcolo
 {
   public:
     template< class Arg > Arg operator()( Arg& a ) const
     {
       return membro(a) ;
     }
   private:
     Obj membro ;
 } ;

Istanziando la classe template calcolo, utilizzando come parametro la classe chiara (ossia istanziando calcolo<chiara>), la funzione oggetto di calcolo avrà sempre lo stesso tipo di ritorno di quello della funzione oggetto di chiara.

Se invece istanziamo la classe calcolo utilizzando la classe confusa (ossia istanziando calcolo<confusa>):

 struct confusa
 {
   double operator()( int    ) ; // Il tipo del parametro
   int    operator()( double ) ; // NON è uguale al tipo ritorno.
 } ;

Il tipo di ritorno della funzione oggetto di calcolo non sarà lo stesso di quello della classe confusa (potrà esserci una conversione da int a double o viceversa, a seconda dell'istanziazione di calculus<confused>.operator()).

La nuova libreria, proposta per il prossimo standard, introduce la classe template result_of, che permette al programmatore di determinare ed utilizzare il tipo di ritorno di una funzione oggetto per qualsiasi dichiarazione. Nella versione corretta calcolo_ver2 viene impiegata la nuova utility per ricavare il tipo di ritorno della funzione oggetto:

 template< class Obj >
 class calcolo_ver2
 {
   public:
     template< class Arg >
     typename result_of<Obj(Arg)>::type operator()( Arg& a ) const
     { 
       return membro(a) ;
     }
   private:
     Obj membro ;
 } ;

In questo modo nelle istanziazioni della funzione oggetto di ‘calcolo_ver2<confusa>' non ci saranno più conversioni.

Il problema di determinare il tipo di ritorno di una chiamata ad un oggetto funzione è un sottoinsieme del problema più generale di determinare il tipo di risultato di un'espressione. Questo problema potrebbe essere risolto in futuro espandendo le funzionalità della typeof a tutte le casistiche possibili.

C++ Standards Committee Papers

[modifica | modifica wikitesto]
  • ISO/IEC DTR 19768 (19 ottobre, 2005) Doc No: N1905 Working Draft, Standard for programming Language C++
  • ISO/IEC DTR 19768 (24 giugno, 2005) Doc No: N1836 Draft Technical Report on C++ Library Extensions
  • Lawrence Crowl (2 maggio, 2005) Doc No: N1815 ISO C++ Strategic Plan for Multithreading
  • Detlef Vollmann (24 giugno, 2005) Doc No: N1834 A Pleading for Reasonable Parallel Processing Support in C++
  • Lawrence Crowl (25 agosto, 2005) Doc No: N1874 Thread-Local Storage
  • Jan Kristoffersen (21 ottobre, 2002) Doc No: N1401 Atomic operations with multi-threaded environments
  • Hans Boehm, Nick Maclaren (21 aprile, 2002) Doc No: N2016 Should volatile Acquire Atomicity and Thread Visibility Semantics?
  • Lois Goldthwaite (2 febbraio, 2004) Doc No: N1592 Explicit Conversion Operators
  • Francis Glassborow, Lois Goldthwaite (5 novembre, 2004) Doc No: N1717 explicit class and default definitions
  • Bjarne Stroustrup, Gabriel Dos Reis (11 dicembre, 2005) Doc No: N1919 Initializer lists
  • Herb Sutter, Francis Glassborow (6 aprile, 2006) Doc No: N1986 Delegating Constructors (revision 3)
  • Michel Michaud, Michael Wong (6 ottobre, 2004) Doc No: N1898 Forwarding and inherited constructors
  • Bronek Kozicki (9 settembre, 2004) Doc No: N1676 Non-member overloaded copy assignment operator
  • R. Klarer, J. Maddock, B. Dawes, H. Hinnant (20 ottobre, 2004) Doc No: N1720 Proposal to Add Static Assertions to the Core Language (Revision 3)
  • V Samko; J Willcock, J Järvi, D Gregor, A Lumsdaine (26 febbraio, 2006) Doc No: N1968 Lambda expressions and closures for C++
  • J. Järvi, B. Stroustrup, D. Gregor, J. Siek, G. Dos Reis (12 settembre, 2004) Doc No: N1705 Decltype (and auto)
  • B. Stroustrup, G. Dos Reis, Mat Marcus, Walter E. Brown, Herb Sutter (7 aprile, 2003) Doc No: N1449 Proposal to add template aliases to C++
  • Douglas Gregor, Jaakko Järvi, Gary Powell (10 settembre, 2004) Doc No: N1704 Variadic Templates: Exploring the Design Space
  • Gabriel Dos Reis, Bjarne Stroustrup (20 ottobre, 2005) Doc No: N1886 Specifying C++ concepts
  • Daveed Vandevoorde (14 gennaio, 2005) Doc No: N1757 Right Angle Brackets (Revision 2)
  • Walter E. Brown (18 ottobre, 2005) Doc No: N1891 Progress toward Opaque Typedefs for C++0X
  • J. Stephen Adamczyk (29 aprile, 2005) Doc No: N1811 Adding the long long type to C++ (Revision 3)
  • Chris Uzdavinis, Alisdair Meredith (29 agosto, 2005) Doc No: N1827 An Explicit Override Syntax for C++
  • Herb Sutter, David E. Miller (21 ottobre, 2004) Doc No: N1719 Strongly Typed Enums (revision 1)
  • Matthew Austern (9 aprile, 2003) Doc No: N1456 A Proposal to Add Hash Tables to the Standard Library (revision 4)
  • Doug Gregor (8 novembre, 2002) Doc No: N1403 Proposal for adding tuple types into the standard library
  • John Maddock (3 marzo, 2003) Doc No: N1429 A Proposal to add Regular Expression to the Standard Library
  • P. Dimov, B. Dawes, G. Colvin (27 marzo, 2003) Doc No: N1450 A Proposal to Add General Purpose Smart Pointers to the Library Technical Report (Revision 1)
  • Doug Gregor (22 ottobre, 2002) Doc No: N1402 A Proposal to add a Polymorphic Function Object Wrapper to the Standard Library
  • D. Gregor, P. Dimov (9 aprile, 2003) Doc No: N1453 A proposal to add a reference wrapper to the standard library (revision 1)
  • John Maddock (3 marzo, 2003) Doc No: N1424 A Proposal to add Type Traits to the Standard Library
  • Daveed Vandevoorde (18 aprile, 2003) Doc No: N1471 Reflective Metaprogramming in C++
  • Jens Maurer (10 aprile, 2003) Doc No: N1452 A Proposal to Add an Extensible Random Number Facility to the Standard Library (Revision 2)
  • Walter E. Brown (28 ottobre, 2003) Doc No: N1542 A Proposal to Add Mathematical Special Functions to the C++ Standard Library (version 3)
  • Douglas Gregor, P. Dimov (9 aprile, 2003) Doc No: N1454 A uniform method for computing function object return types (revision 1)
  • The C++ Source Bjarne Stroustrup (2 gennaio, 2006) A Brief Look at C++0x
  1. ^ C++11 - cppreference.com, su en.cppreference.com. URL consultato il 13 marzo 2020.
  2. ^ C++11 FAQ, su www2.research.att.com. URL consultato il 2 novembre 2010 (archiviato dall'url originale l'11 maggio 2011).
  3. ^ We Have FDIS! (Trip Report: March 2011 C++ Standards Meeting) « Sutter's Mill
  4. ^ We have an international standard: C++0x is unanimously approved « Sutter's Mill
  5. ^ ISO/IEC 14882:2011 - Information technology - Programming languages - C
  6. ^ Buy ISO/IEC 14882 ed3.0 - Information technology - Programming languages - C++ | IEC Webstore | Publication Abstract, Preview, Scope
  7. ^ When compilers, su www2.research.att.com. URL consultato il 2 novembre 2010 (archiviato dall'url originale l'11 maggio 2011).
  8. ^ C/C++ Users Journal Bjarne Stroustrup (maggio, 2005) The Design of C++0x: Reinforcing C++'s proven strengths, while moving into the future
  9. ^ Bjarne Stroustrup Expounds on Concepts and the Future of C, su devx.com. URL consultato il 10 settembre 2011 (archiviato dall'url originale il 7 gennaio 2012).
  • Web Log di Raffaele Rialdi (16 settembre, 2005) Il futuro di C++ raccontato da Herb Sutter
  • Informit.com (5 agosto, 2006) The Explicit Conversion Operators Proposal
  • Informit.com (25 luglio, 2006) Introducing the Lambda Library
  • Dr. Dobb's Portal Pete Becker (11 aprile, 2006) Regular Expressions TR1's regex implementation
  • Informit.com (25 luglio, 2006) The Type Traits Library
  • Dr. Dobb's Portal Pete Becker (11 maggio, 2005) C++ Function Objects in TR1

Collegamenti esterni

[modifica | modifica wikitesto]
  Portale Informatica: accedi alle voci di Wikipedia che trattano di informatica