Il C++14 è il nome informale della prossima revisione dello standard C++ ISO/IEC che potrebbe essere ufficializzata quest’anno. La bozza approvata dal comitato ISO – N3797 – è stata pubblicata il 15 Maggio 2013.
In questo breve articolo vediamo alcune delle features più interessanti già disponibili su Clang (ogni argomento ha il link al relativo paper/draft). Vi diamo la possibilità di provare alcuni esempi direttamente nell’articolo. Qualsiasi autore può utilizzare nei propri articoli questi “snippet compilabili”, quindi se avete voglia di scrivere un articolo del genere fatevi sotto!
Generic lambdas & initialized capture
Scrivendo una lambda, quante volte vi siete chiesti: “ma perché il compilatore non deduce il tipo dei parametri automaticamente?!” Per esempio in un for_each:
vector<int> v; for_each(begin(v), end(v), [](int i) { cout << i << endl; });
Per prima cosa il compilatore sa quale tipo ci va, ma non solo: la stessa lambda (a parte il parametro) potrebbe essere riutilizzata altrove, per stampare qualsiasi oggetto che supporti l’operator<<(ostream&):
auto printer = [](string s) { cout << s << endl; };
In C++14 è possibile creare delle lambda generiche (dette anche polimorfe), tramite auto:
#include <iostream> using namespace std; int main() { auto printer = [](auto value) { cout << "PRINTING: " << value << endl; }; printer(10); printer("hello"); }
Un lambda stateless (con cattura “vuota”), proprio come nel C++11, si può castare ad un puntatore a funzione appropriato:
auto printer = [](auto value) { cout << value << endl; }; void(*printIntFn)(int) = printer;
Un altro limite delle lambda riguarda la cattura che è consentita solo per copia e per reference, escludendo, di fatto, una cattura “by-move”. Si possono adottare alcuni workaround – come bind oppure nascondere una move sotto una copy – ma si tratta, appunto, solo di trucchi per eludere un limite del linguaggio.
Nel C++14 la sintassi della capture-list permette delle vere e proprie inizializzazioni di variabili (questa nuova caratteristica è chiamata, infatti, initialized lambda capture):
#include <iostream> #include <memory> #include <vector> #include <numeric> using namespace std; int main() { unique_ptr<int> ptr {new int{10}}; auto closure = [ptr = move(ptr)]{ cout << "From closure: " << *ptr << endl; }; closure(); if (ptr) // is ptr valid? cout << "ops...move didn't work..." << endl; vector<int> v{1,2,3,4,5}; auto printSum = [sum = accumulate(begin(v), end(v), 0)]{ cout << "Sum is: " << sum << endl; }; printSum(); }
Suggeriamo di non abusare di questa notazione…Non è il caso di scrivere tutto il codice tra [ ] 🙂
Notevole di questa sintassi è il poter creare delle closure con “full ownership”: nell’esempio di prima, se avessimo introdotto uno shared_ptr la lambda avrebbe in qualche modo condiviso la proprietà del puntatore con lo scope in cui è definita. Al contrario, muovendo uno unique_ptr dentro la lambda si sta completamente trasferendo la proprietà del puntatore all’interno della stessa. Non mancano casi in cui questa nuova sintassi farà davvero comodo, specialmente in ambito multi-thread.
Return type deduction for normal functions
In C++11 il tipo di ritorno di una lambda viene dedotto automaticamente se questa è composta di una sola espressione. Nel C++14 la deduzione del tipo di ritorno di una lambda viene estesa anche per casi i più complicati.
Ma non solo: la deduzione automatica è abilitata anche per le funzioni ordinarie, tramite due diverse notazioni:
// auto-semantics auto func() {...} // decltype-semantics decltype(auto) func() {...}
Nel primo caso il tipo di ritorno è dedotto seguendo la semantica di auto (e.g. &-qualifiers eliminati), nel secondo quella di decltype. Vediamo un esempio con auto:
#include <iostream> #include <type_traits> #include <vector> using namespace std; template<typename T> auto sum(T&& a, T&& b) { return a+b; } int main() { cout << "Summing stuff:" << endl; cout << sum(1, 2) << endl; cout << sum(1.56, 3.66) << endl; cout << sum(string{"hel"}, string{"lo"}) << endl; }
Uno scenario nel quale questa notazione sarebbe poco appropriata (l’esempio non compila, di fatto):
#include <memory> using namespace std; struct Base {}; struct Derived : Base {}; shared_ptr<Base> share_ok(int i) // ok { if (i) return make_shared<Base>(); return make_shared<Derived>(); } auto share_ops(int i) // ops { if (i) return make_shared<Base>(); return make_shared<Derived>(); } int main() { auto shared1 = share_ok(1); auto shared2 = share_ops(1); }
In ogni caso, prima di dare linee guida o suggerimenti stilistici, attendiamo di utilizzare questa feature in produzione.
Ed ecco auto e decltype(auto) a confronto:
#include <iostream> #include <type_traits> #include <vector> using namespace std; vector<int> v{1,2,3,4}; decltype(auto) getDecltype(size_t index) { return v[index]; } auto getAuto(size_t index) { return v[index]; } int main() { auto val = getAuto(0); auto anotherVal = getDecltype(1); auto& ref = getDecltype(0); ref += 10; // aka: v[0] += 10; cout << "copied v[0] = " << val << endl; cout << "copied v[1] = " << anotherVal << endl; cout << "final vector: [ "; for (auto i : v) cout << i << " "; cout << "]" << endl; }
Questo facile esempio mostra che, nonostante l’operator[] di un vector riporti una reference, la semantica di deduzione di auto vuole che i ref-qualifiers siano eliminati. Per questo getAuto() restituisce un int (per copia). Viceversa, con decltype(auto), vengono utilizzate le regole deduttive di decltype che preservano i qualificatori (e quindi il fatto che operator[] riporti una reference). Per una spiegazione più accurata di auto e decltype vi consigliamo questo articolo di Thomas Becker. Provate a giocare con l’esempio direttamente nell’articolo. Se siete soliti scrivere codice generico, troverete molto utili queste due novità!
Variable templates
Nel C++11 non c’è modo di parametrizzare una costante direttamente con i template, come invece è possibile per classi e funzioni. Generalmente vengono utilizzati dei workarounds come classi al cui interno sono definite una serie di static constexpr (e.g. vedi numeric_limits).
Dal C++14 è consentito definire delle constexpr variable templates (o solo variable templates), come ad esempio:
#include <iostream> #include <iomanip> using namespace std; template<typename T> constexpr T pi = T(3.1415926535897932385); template<typename T> T areaOfCircle(T r) { return pi<T> * r * r; } int main() { cout << setprecision(10); cout << "PI double = " << pi<double> << endl; cout << "PI float = " << pi<float> << endl; cout << "Area double = " << areaOfCircle(1.5) << endl; cout << "Area float = " << areaOfCircle(1.5f) << endl; }
La nostra carrellata del C++14 è completa. Non esitate a lasciare un commento con le vostre impressioni. E se un commento non vi basta, scrivete un intero articolo! Contattateci e vi aiuteremo a pubblicare il vostro “pezzo” su ++it – i mini-compilatori sono disponibili per tutti!