Guarda tutte le foto!
Lo scorso 26 Febbraio, ai Community Days 2014, abbiamo curato una traccia dedicata al C++. Il feedback è stato positivo e ci siamo divertiti molto. Come per l’Agile Day, scrivo un piccolo wrap-up del mio talk e della giornata in generale.
Ospite d’eccezione per la nostra track è stato Alessandro Contenti (Visual C++ Principal Development Manager @Microsoft Corp). La giornata inizia con tutti i nostri speaker sul palco della sala principale e Raffaele Rialdi (responsabile della nostra agenda e ambasciatore di ++it per questo evento) presentando brevemente la nostra track e la community:
Spostandoci in Sala 4 (la sala dedicata alla track C++), inizio io con la sessione sulle novità del linguaggio: “C++11 in Azione”.
C++11 in Azione
Le slides sono sul sito ufficiale dei CDays14.
Il talk è un’introduzione ad alcune delle novità principali del C++11. Piuttosto che fare un mero elenco di features, ho preferito ripartire dalle meccaniche base del linguaggio e rivisitarle, mostrando come si “pensa direttamente in C++ oggi”. Ho diviso, quindi, la sessione in cinque macro-argomenti principali, di seguito descritti.
Inizializzazione
Il C++11 offre nuovi strumenti che rendono possibile l’inizializzazione di variabili seguendo tre semplici regole. Gli ingredienti di queste regole sono, ovviamente: auto, initializer lists e uniform initialization.
- Per inizializzare un oggetto di tipo T (in ogni contesto – e.g. variabile locale, member-variable, …) usa la {}-initialization:
Developer dev{"Marco", 26}; auto dev = dev{"Marco", 26};
- Le due eccezioni alla regola sopra, per le quali usare la ()-initialization, sono:
auto x = int(2.5); // narrowing - comunque da evitare (è come un C-cast) auto v = vector<int>(5, 1); // no init-list constructor
- Per il tutto il resto usare auto:
auto file = GetFile("cdays14.txt"); auto sum = x+y; const auto& elem = vec[0];
Value semantics
Quello che differenzia il C++ dagli altri linguaggi è la possibilità di controllare con precisione il lifetime degli oggetti. Normalmente gli oggetti sono passati per valore (per copia). Questo implica che un oggetto è diverso da un altro per il suo “contenuto” e non per un qualche genere di identificatore. Aggiungendo esplicitamente qualificatori è possibile adottare un’altra semantica (e.g. & per la reference semantics).
Obiettivo dei programmatori quanto dei compilatori è la minimizzazione di queste copie, con idiomi, ottimizzazioni, trucchi. Il C++11 rende finalmente disponibile ai programmatori la preziosa opportunità di avere a che fare con oggetti temporanei, ovvero “che stanno per essere distrutti” o che sono stati creati dal compilatore come passaggio intermedio di un’operazione più complessa.
Questa opportunità è data dalla move semantics, che rende possibile il controllo della costruzione di un oggetto a partire da un temporaneo. Si pensi, ad esempio, a quando un vector viene ritornato per valore da una funzione: anziché copiare, il compilatore preferisce “muovere” (se l’operatore è disponibile) il che è generalmente un’operazione economica e spesso exception-safe (e.g. swap di due puntatori). Nonostante i compilatori effettuino un’ottimizzazione della copia da decenni (e.g. copy-elision) questa non è garantita dallo standard.
Ownership e lifetime
In C++ la distruzione degli oggetti è deterministica: il linguaggio garantisce che una variabile automatica (locale) venga distrutta appena esce dal suo scope di definizione. Questa garanzia forte dà vita all’idioma più importante del linguaggio: RAII (Resource Acquisition Is Initialization). L’idea è di wrappare ogni risorsa (che in questo senso non vuol dire solo risorsa di sistema ma anche un oggetto del nostro dominio) di modo che essa venga acquisita quando il wrapper viene costruito e rilasciata quando esso viene distrutto:
vector<string> GetLines(const string& path) { ifstream file{path}; // file.open(); // ... get lines } // file.close() automatico
Questa sintassi non è solo elegante e compatta ma anche exception-safe, grazie alla garanzia di distruzione di una variabile locale. Questo principio è alla base dell’ownership in C++. Da qui è possibile costruire policy di ownership in base alla semantica che si dà alle operazioni di copia (e ora anche di move).
Un wrapper non copiabile né movibile è detto guardia e modella l’ownership più semplice: quella di tipo scoped (una risorsa viene “posseduta” solo all’interno di uno scope, non può uscire). Un esempio tratto dalla libreria standard è quello del lock_guard.
Cosa fare se è necessario “passare” l’ownership da uno scope all’altro? Una soluzione ingenua potrebbe prevedere l’uso spudorato di allocazione dinamica e ownership “manuale”. Questo implicherebbe una serie di problemi discussi anche qui. Ma come unire i benefici delle variabili automatiche (e della RAII) con l’allocazione dinamica? Utilizzati per decenni, il C++11 rende disponibili nuovi smart pointers non intrusivi, ovvero proxy a puntatori raw (“unmanaged”) che si prendono carico di gestirne la proprietà in base ad una ben precisa policy di ownership.
Gli shared_ptr modellano una policy di proprietà condivisa tramite ref-counting, mentre gli unique_ptr sfruttano la move semantics per modellare una policy di tipo unique. Da qui consegue che la move semantics non è solo un’opportunità di ottimizzazione e una rivoluzione stilistica ma anche lo strumento che permette di modellare una policy di unique ownership in modo elegante e compatto.
Le semplici regole dell’ownership in C++11 sono di seguito riportate:
- Proteggi le risorse con RAII;
- Preferisci lifetime automatico e scoped ownership;
- Abilita copy/move, se necessarie;
- Se devi allocare dinamicamente ri-usa contenitori standard (e.g. smart pointers, containers, …) oppure scrivine uno confinando new e delete solo in costruttore/distruttore (o analoghi);
- Tendi alla Rule of Zero.
La Rule of Zero è un’applicazione del Single Responsability Principle e afferma che se una classe ha a che fare esclusivamente con l’ownership allora può personalizzare costruttore di copia e move, operatori di assegnazione/move e distruttore. Altrimenti non deve personalizzare alcuno di questi operatori (il compilatore crea per noi quelli di default, member-wise).
Due esempi: una classe File che abbia che fare con l’ownership di un FILE* (alla C) ha il permesso di personalizzare la semantica dei suoi operatori (e.g. disabilitando la copia, trasferendo l’ownership del FILE* tramite move e facendo una fclose in distruzione). Di contro, un repository di File, modellato solo con una member-variable di tipo vector<File>, non deve personalizzare alcun operatore, perché il compilatore sintetizzerà una versione di default che andrà bene (chiamando gli operatori del vector<File>).
Iterazione
Iterazione in C++11 vuol dire, in molti casi, dimenticarsi degli iteratori. Per visitare tutti gli elementi di un range (e.g. una coppia begin-end) è possibile avvalersi del range-based for loop:
void Graph::Accept(IVisitor& visitor) { for(auto& node : nodes) { visitor.Visit(node); } }
Per cicli con logica è preferibile utilizzare un algoritmo, perché no, veicolato da una lambda:
auto evenCount = count_if(begin(nums), end(nums), [](int i) { return i%2 == 0; });
Lambdas
Una lambda expression è una shortcut sintattica per creare inline dei function objects (funtori o callable-objects) anonimi (senza nome e con tipo non specificato). Hanno due possibili declinazioni:
Funzioni anonime stateless (castabili a function-pointers):
auto it = find_if(begin(nums), end(nums), [](int i) { return i%2 == 0; });
Closures (con accesso ad alcune/tutte le variabili nello scope):
auto sum = 0; auto weight = 2; for_each(begin(nums), end(nums), [&sum, weight](int i) { sum += i * weight; });
La cattura delle variabili nello scope può avvenire per copia o per riferimento.
Per fare storage di una lambda (e per passarla) è possibile utilizzare auto (o un template se va messa in un class-member o va passata ad una funzione), che costituisce la scelta più performante. Spesso questo non è possibile (e.g. creare un vector di lambdas) e allora è possibile utilizzare un nuovo contenitore standard di funzioni e callable-objects: std::function. Questo è generalmente un po’ meno performante perché – essendo un contenitore “polimorfo” – è spesso implementato utilizzando type-erasure (quindi allocazione dinamica) anche se i compilatori sono sempre nostri amici e bravi ottimizzatori.
Il talk si conclude con una sfumatura verso il C++14, con introduzione della initialized capture delle lambdas, chiamata in causa dal limite di non poter fare una cattura per move e quindi non poter trasferire l’ownership di una risorsa all’interno di una lambda.
Alcune domande
Sono davvero contento della platea! Preparata ed interessata, ho fatto alcune domande live e sono sempre riuscito a ricevere risposte, regalando un gadget! Perdonatemi, mi ricordo solo due domande che mi avete fatto (se ne ricordate altre o volete farne altre scrivetele in un commento per favore):
- Come inizializzo tramite auto un tipo numerico diverso da int?
R: con un literal (e.g. auto a = 10.0f – float; auto size = 100u; – unsigned).
- Gli shared_ptr sono thread-safe?
R: l’incremento del contatore è atomico, quindi sì. L’utilizzo della risorsa dev’essere però sincronizzato dal programmatore.
Altre sessioni
Dopo il mio talk è stata la volta di Guido Pederzini che ha parlato di Visual Studio 2013 e alcuni tools indispensabili alla realizzazione di prodotti di alto livello. Debugger, profiler, analisi statica e warnings sono solo alcuni esempi.
Segue poi Raffaele Rialdi con una panoramica dettagliata sulle estensioni C++/CX per scrivere ed utilizzare efficacemente oggetti WinRT.
Dopo è salito sul palco Alessio Gogna che ha mostrato un po’ di STL e boost con esempi live. Il suo compito, non banale, è riuscito comunque molto bene, trattando le tematiche con grande simpatia e competenza.
Finalmente è poi stato il turno di Ale Contenti e il suo divertentissimo talk su Cinder e OpenFrameworks ricco di esempi e demo che hanno fatto venir voglia di comprare un Surface e scrivere app!
Chiude la giornata la sessione di Raf e Ale sulla migrazione da legacy a moderno. Il talk riprende molti argomenti trattati in precedenza (dalla RAII agli smart pointers, dai warning all’SDL) e li porta in un contesto legacy, mostrandone i benefici in maniera evidente.
Hanno colorato l’intera giornata i nostri orginalissimi “Cheatsheet C++11”, ovvero dei pieghevoli in 6 facciate (un A4) con le novità più importanti del C++11 divise per categorie. Un grazie speciale a Franco Milicchio per la parte grafica. Oltre ai cheatsheet abbiamo regalato qualche gadget (magliette, mouse-pad e tazze)!
Mi sembra giusto e doveroso un ringraziamento per questa track, sia nei confronti di chi ci ha messo faccia e impegno sia verso chi ha voluto e permesso che una track su C++ fosse possibile all’interno dei community days: sviluppo in C#, provengo da VB, non ho mai avuto a che fare con C++ ma ho seguito con curiosità e interesse crescente tutta la track.
Spero di riuscire a dedicarmi un po’ a questo linguaggio, che mi incuriosisce dal punto di vista personale: non mi prefiggo l’obbiettivo di lavorarci, al momento non ha molto a che vedere con ciò che faccio. Ma la sensazione a partire dalla track ai CD è che conoscere anche solo decentemente C++ possa aiutarmi a scrivere meglio anche in altri linguaggi; il fatto che imponga necessariamente di pensare a ciò che si instanzia e che quindi costringa ad una certa autodisciplina è qualcosa che chi come me è abituato a far conto su qualche using e al garbage collector non tiene sempre sempre da conto.
Grazie ancora a tutti!
Ciao Matteo, grazie a te per la fiducia e l’interesse che hai dimostrato per la nostra track. Speriamo di organizzare nuovi eventi/meeting e di incontrarci ancora! A presto!