basic – Italian C++ Community https://www.italiancpp.org Mon, 24 Aug 2020 13:03:53 +0000 it-IT hourly 1 https://wordpress.org/?v=4.7.18 106700034 Modern range-based iteration https://www.italiancpp.org/2013/09/23/modern-range-based-iteration/ https://www.italiancpp.org/2013/09/23/modern-range-based-iteration/#comments Mon, 23 Sep 2013 07:29:47 +0000 http://www.italiancpp.org/?p=1201 La libreria standard è basata sul concetto di iteratore,  ovvero un oggetto che “punta” ad un elemento di un range e che consente di “spostarsi” (iterare) attraverso gli altri elementi, imitando per quanto possibile la sintassi usata con i puntatori. Tutti gli iteratori sono classificati in base ad alcune categorie, all’incirca a seconda che permettano lettura e/o scrittura, e a seconda di che tipi di spostamento supportano (questa spiegazione va oltre lo scopo dell’articolo – basic – ma per saperne di più è possibile partire da qui). In particolare, un vero puntatore è un caso particolare di iteratore:

int arr[] = {1,2,3,4};
int* ptr = &arr[1]; // ptr punta a 2
++ptr; // ptr punta a 3
*ptr = 10;
// arr è ora {1,2,10,4}

Questo articolo tratta alcune linee guida a proposito di iterazione su range. Un range è inteso come due iteratori che rappresentano l’inzio e la fine dell’intervallo di elementi sui quali iterare. Non si tratta, però, di iteratori qualsiasi bensì di quelli che lo standard definisce come InputIterator. Questo categoria di iteratori è la più semplice e consente di scorrere serialmente gli elementi in “sola lettura” (solo input – a “scrivere” sono gli OutputIterator). Per essere più rigorosi (ma non troppo), un InputIterator è un qualsiasi tipo di oggetto che supporta almeno queste operazioni:

  • value-semantics (ha un costruttore di copia, un operator= e un distruttore pubblici)
  • si può testare l’uguaglianza (operator==, operator!=)
  • può essere dereferenziato (*it, it->member), ma il risultato va trattato come se fosse di sola lettura (si può scrivere auto x = *it; ma non *it = 10 come sopra)
  • può essere incrementato di 1 (con operator++)

Altre operazioni sono implicate (ad esempio,  la funzione swap) o supportate con certe limitazioni (il costruttore di default). Si noti però che non si parla mai della sequenza di oggetti “puntati”, che infatti potrebbe addirittura… non esistere! Un InputIterator potrebbe “fingere” di iterare su elementi che vengono creati al volo nell’istante in cui si de-referenzia (si vedrà un esempio più avanti). Secondo lo standard, de-referenziando due copie dello stesso iteratore non necessariamente si ottiene lo stesso valore!

Comunque, l’idea fondamentale è che è necessario definire alcune funzioni anziché ereditare da una classe base. Qualsiasi classe che “si comporta” come un InputIterator è allora considerata un InputIterator. Si tratta del classico approccio della generic programming, meglio formalizzato nei Concepts.

Spesso ci troviamo a scrivere cicli su strutture dati, ad esempio su vettori o liste. La libreria standard fornisce diversi algoritmi che internamente operano come dei cicli. Il più semplice è il for_each, che dati due InputIterator Begin/End esegue una certa funzione per ogni elemento nel range [Begin, End).

Un container può essere “descritto” da un range [Begin, End) e per questo definisce le quattro funzioni:

  • begin()cbegin() (ovvero const-begin, che produce un iteratore di sola lettura)
  • end()cend() (ovvero const-end)

begin() è, chiaramente, un InputIterator che punta al primo elemento del container, end(), invece, punta subito fuori dal range (one-past-the-last-element). Ci sono diverse motivazioni per questa scelta architetturale, ma (semplificando) è possibile pensare che questa sia in linea col classico pattern:

for (int i=0; i!=END; ++i) // END escluso
...

con i = END il ciclo si arresta, quindi l’ultima iterazione è proprio con i = END – 1.

Nota: gli iteratori ritornati dalle funzioni begin/end di un container sono generalmente più avanzati di un InputIterator (ad esempio possono supportare contemporaneamente input e output), ma tutti comunque ne supportano le specifiche.

Supponiamo, ora, di avere una classe Plot che supporta un replot:
class Plot
{
public:
...
void replot();
...
};

dato un vector di Plot potremmo ridisegnare ogni plot con un banale ciclo, sfruttando le funzioni sopra citate:
vector<Plot> plots;
for (auto it = plots.begin(); it != plots.end(); ++it)
{
i->replot();
}

Ma potremmo usare anche un for_each:
for_each(plots.begin(), plots.end(), [](Plot& plot)
{
plot.replot();
});

Cosa scegliere e perché? Ci sono almeno quattro buone ragioni per le quali scegliere un algoritmo invece di scrivere un ciclo:

1) Efficienza: un algoritmo è potenzialmente vincente perché:

  • evita di ripetere alcune chiamate (e.g. plots.end()), mantenendo il codice compatto,
  • gli implementatori delle STL possono specializzare particolari algoritmi in base al container, massimizzando le performance,
  • (generalmente) gli algoritmi sono sofisticati e implementati rispettando lo stato dell’arte (e.g. sort). anche algoritmi apparentemente banali, come potrebbe essere “visitare tutti gli elementi” possono essere ottimizzati e nascondere  gradi di sofisticazione insospettabili!

2) Correttezza: scrivere un ciclo a mano è error-prone, richiede di controllare la validità del range in cui si itera e potenzialmente fa perdere più tempo a scrivere controlli che non a implementare il core del proprio algoritmo.

3) Manutenibilità: è più facile mettere mano su un codice che usa algoritmi standard, documentati e provati, oppure su listati completamente scritti a mano (e più verbosi)? Generalmente è molto più semplice leggere e comprendere un codice che utilizza algoritmi standard ed uno dei motivi principali è che questi ultimi sono ben documentati (e i loro nomi hanno un significato che esprime l’intento del programmatore). Essi rappresentano un vero e proprio “vocabolario” con cui comunicare (ad esempio nel proprio team) e dal quale attingere funzionalità già pronte.

4) Astrazione: non sempre occorre avere a che fare con gli iteratori. Con un ciclo scritto a mano è necessario utilizzarli, dereferenziarli, … Con un algoritmo, invece, è possibile utilizzare direttamente l’oggetto “puntato”, come nell’esempio sopra, dove accediamo ad un’istanza di business, Plot, e non ad un iteratore.

Primo DO della giornata: ove possibile, preferire l’utilizzo degli algoritmi ai cicli scritti a mano.

Questo primo DO è generale, non da applicarsi sol al C++. Veniamo al nocciolo di questo post. Iterare su un range: quali sono le nuove linee guida?

All’inizio abbiamo detto che ogni container può essere descritto da un range (Begin. End]. Abbiamo anche fatto vedere come utilizzare le funzioni begin/end per iterare su un container. Il range (Begin, End] è del tipo:

vector<int> vec {1,2,3,4,5};
auto first = vec.begin();
auto one_past_last = vec.end();
// [first, one_past_last) "descrive" vec

L’unico problema è che questo approccio non è abbastanza generico. Ad esempio non può essere applicato agli array C-style (e.g. int arr[]). Come rimediare? Il C++11 introduce le funzioni non-membro begin/end, con un overload per gli array C-style. Oltre a supportare gli array, queste due funzioni danno uniformità e coerenza, promuovendo l’estensibilità (è possibile creare delle specializzazioni per i propri container) e incrementando l’incapsulamento. Le non-member functions begin/end sono fatte così:

// C++11
template< class C >
auto begin( C& c ) -> decltype(c.begin());

// C++11
template< class C >
auto begin( const C& c ) -> decltype(c.begin());

// C++11 - overload per gli array C
template< class T, size_t N >
T* begin( T (&array)[N] );

DOUtilizzare le funzioni non-membro begin(x) e end(x) invece di x.begin() e x.end().

for_each( begin(vec), end(vec), ... ) // C++11
E se avessimo bisogno di cbegin() e cend() ? Il C++14 introduce anche queste due funzioni non-membro, sfuggite al C++11:
// C++14
template< class C >
auto cbegin( const C& c ) -> decltype(std::begin(c));

// C++14
template< class C >
auto cend( const C& c ) -> decltype(std::end(c));

DO: [C++14] Utilizzare le funzioni non-membro cbegin(x) e cend(x) invece di x.cbegin() e x.cend().

Concludiamo questo primo articolo a proposito di iterazione su range con una seconda novità del C++11: il range-based for loop (RBFL). Questo costrutto equivale ad utilizzare un for_each su un container. Stesse garanzie di performance ma più compattezza:

// C++11
for (const auto& elem : vec)
{
cout << elem << " ";
}

// quando vec è una variabile locale, è come scrivere
for_each(begin(vec), end(vec), [](const T& elem)
{
cout << elem << " ";
});

La sintassi è semplice e probabilmente già vista in altri linguaggi. Da notare la possibilità di usare auto. Chiaramente, anche qui, il concetto di iteratore è celato e si opera direttamente sugli oggetti contenuti nel vettore.

DO: Utilizzare il range-based for loop al posto del for_each, è più semplice e compatto.

Il RBFL opera su strutture x che supportino il concetto di iterazione, ovvero:

  • abbiano le funzioni membro x.begin() e x.end(), oppure,
  • abbiano le funzioni non-membro begin(x) e end(x) (trovate con ADL – Argument Dependent Lookup), oppure,
  • per le quali esistono le specializzazioni di std::begin(x) e std::end(x).

Le funzioni devono ritornare degli InputIterator (in realtà il requisito è più “leggero”, come vedremo nel prossimo esempio. Maggiori dettagli qui).

Bonus track

Il RBFL permette di iterare su una qualsiasi struttura “iterabile”. Abbiamo detto poco fa cosa vuol dire, ma vogliamo vederne un esempio pratico?

Supponiamo di voler iterare su tutti gli interi entro un certo intervallo, qualcosa del tipo:

for (int i=0; i<100; ++i)
...

Possiamo usare il RBFL senza creare una sequenza intermedia? Sì, per farlo abbiamo proprio bisogno di scrivere una struttura iterabile e un InputIterator che ne abiliti l’iterazione. Per il nostro scopo, in realtà, dobbiamo solo implementare un wrapper ad un finto container che simuli una lista crescente di numeri ed un suo iteratore.

Per ricapitolare, vogliamo qualcosa del genere:

for (auto i : range{0,10})
{
cout << i << " ";
}
// 0 1 2 3 4 5 6 7 8 9

Ribadisco che questo è solo un esempio, molto probabilmente non efficiente quanto un for da 0 a N!

Iniziamo da questo “finto container”. Abbiamo detto che per usare il RBFL è sufficiente implementare una delle tre opzioni:

  • funzioni membro x.begin() e x.end()oppure
  • funzioni non-membro begin(x) e end(x) (trovate con ADL – Argument Dependent Lookup), oppure
  • specializzazioni std::begin(x) e std::end(x)

Scegliamo la prima e supponiamo, per un attimo, di aver già pensato al nostro InputIterator ad-hoc:
class range_t
{
public:
class range_it
{
// lo vediamo dopo
}

range_t(int s, int e)
: start_it{s}, end_it{e}
{
}

range_it begin()
{
return start_it;
}

range_it end()
{
return end_it;
}

private:
range_it start_it;
range_it end_it;
};

La nostra classe range_t mantiene due iteratori che rappresentano inizio e fine range. Ora vediamo una possibile implementazione di range_it:
class range_it
{
public:
range_it(int val)
: value{val}
{
}

int operator*() const
{
return value;
}

bool operator!=(const range_it& o) const
{
return value != o.value;
}

range_it& operator++()
{
++value;
return *this;
}

private:
int value;
};

L’idea (banale) è di wrappare un valore del range in questo iteratore range_it. Le operazioni su un range_it sono in realtà operazioni sul valore che wrappa.

Non siamo totalmente conformi alle specifiche di un InputIterator ma siamo dentro ai requisiti del RBFL (perché questo ha bisogno solo degli operatori che abbiamo implementato):

for (auto i : range_t{0, 10})
{
cout << i << " ";
}
// 0 1 2 3 4 5 6 7 8 9

L’esempio è volutamente semplice. Restano aperte alcune questioni (anch’esse non complicate da affrontare) che lasciamo ai lettori, come ad esempio:

  • generalizzare il range per qualsiasi tipo numerico (attenzione all’operator!= su float e double…),
  • completare range_it in modo che implementi tutte le specifiche di un InputIterator,
  • personalizzare lo step (e.g. {0, 10} a step di 0.1).
]]>
https://www.italiancpp.org/2013/09/23/modern-range-based-iteration/feed/ 5 1201
Puntatori? Vivi senza! https://www.italiancpp.org/2013/08/23/puntatori-vivi-senza/ https://www.italiancpp.org/2013/08/23/puntatori-vivi-senza/#comments Fri, 23 Aug 2013 08:24:56 +0000 http://www.italiancpp.org/?p=1070 Molte volte il C++, essendo un “figlio” del C, viene identificato con in puntatori. Altro non possiamo dire che “non è vero“.

Ma cominciamo con le basi. Nota bene: nel seguito utilizzeremo termini come “heap” e “stack“, anche se lo standard ISO C++ non ne fa menzione (di fatti non è detto che una piattaforma disponga di stack, ad esempio).

Tutti voi sapete che ogni variabile dichiarata in un blocco di codice, di fatti è allocata in modo automatico sullo stack, una memoria molto veloce ed estremamente limitata:


if (true)
{
int i; // Stack
i = 42;
}

Nell’esempio, la variabile i è allocata sullo stack. Ma cosa succede se dovessimo utilizzare classi molto onerose dal punto di vista dell’occupazione di memoria?

Lo stack non è più una opzione valida, perché è una memoria preziosa. Come hanno insegnato, è necessario allocare tutto sullo heap. E questo sicuramente vi fa venire in mente i puntatori:


if (true)
{
myHugeClass *p; // Stack
p = new myHugeClass(); // Heap
// ...
delete p;
}

Funziona tutto benissimo. Come si nota, il puntatore è allocato sullo stack, è dunque una variabile automatica, e la memoria è deallocata quando si esce dal blocco. Il dato, è invece allocato sullo heap dall’operatore new. Al termine dell’utilizzo della variabile p, dobbiamo ricordarci di deallocare la memoria, e questo è il compito dell’operatore delete.

C’è solo un piccolo problema: e se scordassimo il delete? Benvenuti nel tragico mondo dei memory leak.

La memoria non verrebbe mai deallocata, e la vostra applicazione allocherà nuovamente uno spazio per myHugeClass ogni volta che verrà eseguito il codice. Potenzialmente, potremmo saturare la RAM, con conseguenze catastrofiche. Come ovviare al problema? Essenzialmente dovremmo fare una accoppiata di tutti i new, con un delete, ma questo risulta impraticabile, come vedremo fra breve: non è sempre ovvio dove si trovi una deallocazione.

Smart Pointers

Lo standard ISO C++11 fornisce una soluzione elegante e semplice: utilizzare uno smart pointer, ovvero un puntatore “intelligente”, che dealloca la memoria automaticamente come se fosse sullo stack. La sintassi è estremamente semplice, e l’esempio precedente si riassume in questo codice:


if (true)
{
unique_ptr<myHugeClass> p(new myHugeClass()); // Heap
// C++14: auto p = make_unique<myHugeClass>();
// ...
}

Abbiamo risolto il problema. In questo caso, p è allocato sullo stack, mentre l’istanza di myHugeClass è allocata sullo heap, come ogni puntatore. La cosa “intelligente” degli smart pointers è che alla fine dello scope di p, tutta la memoria verrà automaticamente deallocata, sia ovviamente quello sullo stack, che quella sullo heap. Un delete non è più necessario, e dite addio al memory leak.

Solo per menzionarlo, un altro tipo di smart pointer è lo shared_ptr. Mentre lo unique_ptr consente che ci sia solo un “proprietario” dell’oggetto referenziato, lo shared_ptr permette di condividerne la proprietà:


auto ptr = make_shared<myHugeClass>();
auto ptr2 = ptr; // Istanza condivisa

La vita dell’oggetto puntato viene gestita tramite reference-counting, cioè si contano quanti shared_ptr referenziano l’oggetto puntato e quando uno shared_ptr viene distrutto è come se dicesse “io non sono più interessato alla vita dell’oggetto”. Quando l’ultimo shared_ptr viene distrutto si porta dietro anche l’oggetto puntato e il gioco è fatto!

Per chi viene dal mondo Java, potete vedere il parallelo facilmente: ogni oggetto in Java può essere pensato come uno shared_ptr (con un garbage collector). Solo che in C++ avete la possibilità di scegliere se usare un pointer o uno smart pointer.

Eccezioni

Una ulteriore motivazione sull’uso degli smart pointers al posto dei raw pointers, è nel caso di eccezioni. Prendiamo il secondo esempio. Se allochiamo con new una istanza di myHugeClass, ed il costruttore va in eccezione? A questo punto il codice è inutilizzabile, perché p punta ad una zona di memoria non valida. Cosa dovremmo fare per evitare la catastrofe di utilizzare un pointer invalido? Un semplice trucco consisterebbe nell’uso di try/catch:


if (true)
{
myHugeClass *p = nullptr; // Stack
try
{
p = new myHugeClass(); // Heap
// ...
delete p; // Rilascio la memoria: ho terminato
}
catch(...)
{
delete p; // Rilascio la memoria: errore rilevato
}
}

Sembra semplice, ma questo ci fa venire in mente il problema dell’accoppiamento new con delete. Mentre prima potevamo contare i new, e controllare che il numero di delete fosse uguale, ora non è più valida questa soluzione: abbiamo due deallocazioni, una per un funzionamento fisiologico, una per quello patologico con eccezioni. E la cosa si complica notevolmente con molte variabili e più modi di gestire varie eccezioni.

E dunque gli smart pointers ci aiutano? Certamente: è garantito dallo standard che, nel caso in cui una eccezione venga lanciata, la memoria debba essere automaticamente deallocata. Ecco come diventa l’esempio di prima:


if (true)
{
auto p = make_unique<myHugeClass>(); // C++14 style
} // Il delete è automatico

Conclusioni

Gli smart pointers sono utili classi da utilizzare sempre, ove possibile. Certo è che non è sempre praticabile l’uso degli smart pointers, alcune volte serviranno i cari vecchi raw pointers, ma sono casi particolari. In genere, utilizzare un raw pointer è sconsigliato.

Ma le buone notizie non terminano con questo. Se utilizzate ad esempio i containers, come vector, l’implementazione garantisce che la variabile sia sullo stack, mentre i dati siano allocati dinamicamente sullo heap. E questo non vale ovviamente solo per vector!

Ancora più interessante è il caso in cui voi vogliate usare una funzione che come valore di ritorno ha una istanza molto grande, come ad esempio un vector con molti elementi. Potreste pensare che, associando una variabile al valore di ritorno della vostra funzione, venga copiato ogni elemento dentro il vettore, con chiamate a non finire al costruttore dell’oggetto contenuto nel vector (ad esempio un vettore di myHugeClass): questo sarebbe un overhead enorme. In realtà, dipendentemente dal compilatore però, il C++ fornisce una soluzione automatica, non copiando l’elemento, ma muovendolo, con una tecnica semplice chiamata return value optimization. Ma questa, è un’altra storia.

]]>
https://www.italiancpp.org/2013/08/23/puntatori-vivi-senza/feed/ 24 1070
auto: il linguaggio non è tutto https://www.italiancpp.org/2013/06/24/auto-il-linguaggio-non-e-tutto/ https://www.italiancpp.org/2013/06/24/auto-il-linguaggio-non-e-tutto/#comments Mon, 24 Jun 2013 09:44:06 +0000 http://www.italiancpp.org/?p=625 Molta enfasi di recente è stata posta sul nuovo uso della keyword auto in C++11 (si veda per esempio qui). In sintesi, auto sostituisce un tipo esplicito con una richiesta rivolta al compilatore di riempire con l’informazione corretta:


std::vector<double> v;
auto it = v.begin();

Il frammento sopra afferma: lascio al compilatore la deduzione del tipo di it.

La maggior parte dei commenti però tende a enfatizzare i casi in cui usare auto, ma il vero problema è quando non usarlo. Siccome la keyword risparmia fatica al programmatore, ricordare continuamente “usate auto qui, usate auto là” porta facilmente a pensare che vada usato sempre, e diventa abbastanza naturale abusarne. auto però non è gratis: è fondamentale che il codice esprima correttamente l’intento del programmatore. se l’intento è chiaro, i bug diventano evidenti e si possono correggere facilmente. ma ci sono casi in cui l’uso di auto nasconde l’intento:

  • rileggendo il codice a distanza di tempo, diventa più difficile capire cosa sta succedendo (soprattutto se tutte le variabili locali sono auto… caso realmente accaduto)
  • ci sono casi in cui un cast viene involontariamente eliminato: nell’esempio semplificato sopra, it poteva essere const_iterator, ma il programmatore intendeva dire auto it = v.cbegin() oppure const_iterator it = v.begin()?.
  • (caso particolare del punto precedente) alcuni container restituiscono dei proxy, e l’uso indiscriminato di auto può rompere del codice funzionante


bool f1()
{
std::vector<bool>* vp = new std::vector<bool>(1000, true);
bool y = (*vp)[314]; // ok
delete vp;
return y;
}

bool f2()
{
std::vector<bool>* vp = new std::vector<bool>(1000, true);
auto y = (*vp)[314]; // mmm...
delete vp;
return y; // argh! il proxy potrebbe leggere il container già distrutto
}

  •  un IDE che fa un parsing euristico potrebbe non essere più in grado di elencare correttamente tutti i punti in cui un tipo viene usato; a volte il completamento automatico non funziona più. si pensi ad esempio a:


class ABC
{
int size() const;
};

ABC GimmeMyObject();


// molto più tardi...
auto abc = GimmeMyObject();
auto n = GimmeMyObject().size();

Durante il refactoring, si vogliono trovare tutti gli oggetti di tipo ABC; normalmente basta una ricerca di testo (ci possono essere mille motivi: progetto troppo grosso, o stiamo usando un modem a 56k e vi…), ma se la variabile è auto, ci vuole un IDE più sofisticato e ben integrato con il compilatore.

In sintesi, è una buona idea usare auto quando:

  1. chiunque è in grado di dedurre il tipo senza saltellare attraverso il codice, vuoi per il nome della variabile, vuoi per la semplicità dell’inizializzazione (cfr. esempio #1). Di solito, il tipo è un nome dipendente e lunghissimo (std::map<std::string, std::list<double>, MySpecialComparisonOperator, MyCustomAllocator>::const_iterator…); questa è una buona indicazione per usare auto.
  2. il tipo della variabile potrebbe cambiare in qualsiasi momento, mantenendo la stessa interfaccia. Nell’esempio #2, si pensi che le prime righe siano in realtà generate da un programma esterno che emette codice c++ (ad esempio, questo). Il tipo esatto di “n” potrebbe variare semplicemente aggiornando il programma esterno, ma il cambiamento potrebbe non essere rilevante (spesso basta che sia n un intero con certe proprietà);
  3. quando c’è un limite di  80 caratteri per riga (ma suvvia… siamo nel 2013, chi mai segue una regola del genere?).

Per enfatizzare che il nostro scopo è scoraggiare criticamente, riportiamo anche i casi contrari: è una cattiva idea usare auto quando:

  1. l’inizializzazione non è ovvia, ovvero solo guardando cosa c’è a destra dell’= non è possibile dedurre il tipo della variabile. auto significa “lascio la deduzione al compilatore”, ma non “lascio la deduzione al compilatore… perché io non la so fare” (questo si applica anche alle somme di interi di tipo diverso, p.es. short + unsigned char)
  2. c’è un cast  di mezzo

    auto x = static_cast<int>(GetNumberAsDouble()); // mmm... l'intento è chiaro, ma il codice è contorto
  3. c’è un proxy: auto rischia di tenere in vita degli oggetti che non sono pensati per sopravvivere a lungo
  4. si esagera! non è il caso di iniziare un sorgente con:


auto main(auto argc, const auto* argv[]) -> int // uhm... forse in c++2075...

]]> https://www.italiancpp.org/2013/06/24/auto-il-linguaggio-non-e-tutto/feed/ 1 625 C++ Revolution https://www.italiancpp.org/2013/06/11/cpp-revolution/ https://www.italiancpp.org/2013/06/11/cpp-revolution/#comments Tue, 11 Jun 2013 09:52:40 +0000 http://www.italiancpp.org/?p=450 Da circa un paio d’anni l’interesse per il C++ è aumentato notevolmente. Che sia per la rivoluzione portata dal nuovo standard (C++11) o per necessità industriali, – legate, ad esempio, a scalabilità e performance – è indubbio che il linguaggio sia profondamente radicato in sistemi che usiamo quotidianamente. Ad esempio MySQL, Oracle, ma anche Office, Photoshop e Facebook. E questo link ne raccoglie molti altri.

Il nuovo standard ha cambiato notevolmente il linguaggio, non solo per aver aggiunto elementi inediti alla libreria, ma anche – e soprattutto – per averne mutato lo stile e gli idiomi più classici. Pensiamo, ad esempio, al tornare un oggetto da una funzione. Nel C++98 non è insolito utilizzare uno stile C-like per evitare di ritornare per oggetti per valore. Questo – a meno di ottimizzazioni del compilatore, come il Return-Value Optimization (RVO) – è per tenersi lontano da copie potenzialmente costose. Ora, grazie all’introduzione della move semantics, funzioni di questo genere:


void Calculate(vector<HugeType>& result) 
{
// ... fill result
}

possono essere trasformate in:
vector<HugeType> Calculate()
{
vector<HugeType> result;
// ... fill result
return result;
}

senza incorrere in inutili copie. Questo è garantito dallo standard, senza doversi affidare alle opzioni del compilatore. Più in generale, il C++11 consente di operare con una sintassi chiara su oggetti temporanei o, più in generale, su RVALUE.

Ma il C++11 aggiunge anche diverse facilitazioni per migliorare produttività e sintesi. Come auto, per dedurre automaticamente il tipo di una variabile:

map<string, vector<int>> aMap;

// C++98
map<string, vector<int>>::iterator it = aMap.begin();

// C++11
auto it = aMap.begin();

Oppure il range-based for loop per iterare su un range con una sintassi compatta:
// C++98
for (vector<int>::iterator i = v.begin(); i != v.end(); ++i)
{
cout << *i << " ";
}

// C++11
for (auto i : v)
{
cout << i << " ";
}

Le lambda expressions facilitano e rendono naturale l’utilizzo degli algoritmi e dello stile funzionale:
all_of( begin(vec), end(vec), [](int i){ return (i%2)==0; } );

Le initializer_list estendono la classica inizializzazione con parentesi graffe delle struct, per essere usata in modo personalizzato:

class MyVector
{
public:
MyVector(std::initializer_list<int> list);
...
};

...

MyVector vec = {1,2,3,4,5};

Correlato alle initializer_list, anche il fastidioso problema del most vexing parse è stato risolto, con la uniform initialization. Il C++11 consente di inizializzare qualsiasi oggetto con una sintassi omogenea:

struct BasicStruct
{
int x;
double y;
};

struct AltStruct
{
AltStruct(int x, double y) : x_{x}, y_{y} {}

private:
int x_;
double y_;
};

BasicStruct var1{5, 3.2};
AltStruct var2{2, 4.3};

Proseguendo questa panoramica molto generale, è importante ricordare che anche la libreria standard ha accolto tante novità. A partire dagli smart pointers, deprecando il frainteso auto_ptr:
// C++98
int* anIntPtr = new int(10);

...

delete anIntPtr;

// C++11
unique_ptr<int> anIntPtr( new int(10) ); // will be deleted

Passando poi per nuove strutture dati, come gli unordered container, le tuple, le forward_list, … Anche il supporto alla metaprogrammazione è cresciuto, con l’introduzione di diversi type_traits standard e decltype per inferire il tipo di un’espressione.

E finalmente è possibile scrivere codice multi-thread portabile, sfruttando la libreria nativa:
thread aThread( some_function ); // may be a lambda or any callable obj
thread anotherThread ( another_function );
aThread.join();
anotherThread.join();

Questa panoramica è solo una piccoa parte di tutta la storia. Per tutte le novità del C++11 potete consultare, ad esempio, la pagina relativa su wikipedia.

Non tutto è però gratuito. Per sfruttare al massimo tutte le innovazioni del C++11 (e tra breve del C++14) è necessario comprenderle ed applicarle con disciplina. Non è difficile trovare siti, articoli, tutorial e molto altro su tantissimi aspetti del nuovo standard. Spesso tutto questo volume di informazioni mette in difficoltà chi desidera apprendere gradualmente e non sa da dove iniziare. La prossima sezione raccoglie in modo ordinato alcune delle risorse più importanti ad apprendere e restare aggiornati.

Inoltre, il motivo di questa categoria – DOs & DON’Ts – è proprio quello di suggerire al lettore alcune nuove pratiche e idiomi, rimpiazzando il vecchio stile.

 

C++ Revolution: come inziare

Il C++11 ha mosso molti programmatori C++ verso la riscoperta e la rivisitazione del linguaggio; alcuni si sono ritirati perché convinti in un aumento di complessità, mentre altri ne hanno tratto diversi benefici. Negli ultimi anni sono proliferate risorse, articoli, video e materiale divulgativo per apprendere e approfondire molti aspetti del nuovo standard. Questa breve sezione conclusiva vuole raccogliere in modo ordinato risorse utili per approfondire e restare aggiornati, specialmente per chi è ancora indeciso e smarrito.

 
Consulta isocpp.org periodicamente
 

Il riferimento ufficiale dello standard – da circa novembre 2012 – è isocpp.org. Si tratta dell’unico catalizzatore ufficiale di risorse e news. Consultare periodicamente questo sito consente di restare aggiornati su eventi, libri, video, articoli e molto altro.

 
Considera alcuni testi fondamentali
 

I testi che sento di raccomandare a chi vuole conoscere in modo approfondito le novità del C++11 sono i seguenti:

 
Guarda i video delle ultime conferenze
 

Se desiderate guardare qualche video, consiglierei:

 
Prova diversi compilatori
 

Un altro suggerimento è di provare il proprio codice su diversi compilatori, perché non tutti sono 100% compliant col C++11 (ad oggi solo GCC). Grazie ad alcuni compilatori online è possibile compilare ed eseguire direttamente dal browser.

 
Consulta la categoria DOs & DON’Ts!
 

Come annunciato, l’obiettivo della nostra categoria DOs & DON’Ts è quello di suggerire nuovi idiomi e pratiche stilistiche, rimpiazzando il vecchio modo di programmare in C++, ove possibile. Preferiamo la sinteticità del codice. Non mancheranno, quindi, snippet con confronti “ieri/oggi” e link a codice da compilare e provare direttamente online. Speriamo, poi, di poter discutere con i lettori non solo nei commenti ma anche (e soprattutto) nei vari gruppi di discussione.

 

Chiaramente, invitiamo chiunque voglia contribuire a farlo!

 

 

]]>
https://www.italiancpp.org/2013/06/11/cpp-revolution/feed/ 2 450