Alcune nuove funzionalità di C++ che tutti gli sviluppatori dovrebbero conoscere

Alcune nuove funzionalità di C++ che tutti gli sviluppatori dovrebbero conoscere

C++ ha subito una forte evoluzione nel tempo.

Ovviamente ciò non è avvenuto nel corso di una notte. C’era un tempo in cui C++ peccava di dinamismo. Era difficile che molti programmatori amassero utilizzarlo.

Ma le cose sono cambiate quando si è deciso di introdurre alcune nuove funzionalità.

Dal 2011, C++ si è trasformato nel linguaggio dinamico che molti hanno sempre sperato di utilizzare.

Non pensare che sia diventato facile: rimane comunque uno dei più complessi linguaggi di programmazione, se non il più complesso. Tuttavia è diventato molto più user-friendly rispetto alle versioni precedenti.

Oggi andremo ad analizzare queste nuove features (partendo da C++ 11, che ha già comunque 8 anni di vita) che ogni sviluppatore vorrà sicuramente conoscere.

Nota che tralascerò alcune funzioni avanzate in questo articolo, di cui forse ne parlerò in futuro.

Iniziamo!

La keyword “auto”

Quando C++ 11 ha introdotto auto, tutto è diventato più semplice per noi programmatori.

L’idea che sta alla base, era quella di rendere il compilatore C++ capace di interpretare il tipo di dato al momento della compilazione – invece di dichiararlo ogni-santissima-volta. Ciò è molto conveniente quando abbiamo tipi come: map<string,vector<pait<int,int>>>. 😀

Diamo un’occhiata alla riga 5. Non specificando il tipo di dato, non si può dichiarare una variabile senza inizializzarla. Questa asserzione è corretta: il compilatore non conosce il tipo di dato che vogliamo attribuire alla variabile data, pertanto dobbiamo per forza assegnarne un valore.


Inizialmente, la direttiva auto era in qualche modo limitata. Dopo, nelle versioni successive del linguaggio, il suo utilizzo è diventato più potente.

Nelle righe 7 e 8, vengono inizializzati dei vettori direttamente inline. Questa è inoltre una nuova funzionalità introdotta in C++ 11.

Ricorda, nel caso in cui si utilizzi auto, ci deve essere sempre una via per il compilatore di dedurre il tipo che si vuole assegnare.

Domanda interessante: cose succederebbe se scrivessimo: auto a = {1, 2, 3} ? È un errore di compilazione? È un vettore?

In realtà, C++ ha introdotto std::initializer_list<type>. Le liste inizializzate con auto creeranno un oggetto nella memoria di tipo lista ma vuoto.

Infine, come menzionato precedentemente, il così detto type-deducing effettuato dal compilatore, può essere molto utile quando abbiamo strutture dati molto complesse:

Non dimenticare di analizzare la linea 25! L’espressione auto[v1, v2] = itr.second è letteralmente una nuova funzionalità introdotta in C++ 17. È chiamta structured binding (o binding strutturato). Nelle versioni precedenti del linguaggio, avresti dovuto estrarre ogni variabile separatamente. Ma il binding strutturato lo ha reso molto più semplice.

Inoltre, se vuoi estrarre i valori utilizzando le reference, dovrai solo aggiungere un simbolo all’espressione: auto &[v1, v2] = itr.second.

Fine.


Le espressioni lambda

C++ 11 ha introdotto le espressioni lambda, qualcosa come le funzioni anonime di JavaScript. Sono degli oggetti-funzione, senza nessun nome, che catturano le variabili su scopes diversi basate su specifiche sintassi. Sono inoltre assegnabili a variabili.

Le funzioni lambda sono molto utili se hai la necessità di utilizzare semplici funzioni ripetitivamente, e non vuoi scrivere un’intera funzione in un’altra porzione di codice per fare ciò. Un altro interessante utilizzo è quello delle funzioni di comparazione.

L’esempio qui sopra ha molto da dire.

Innanzitutto, puoi notare come l’inizializzazione inline semplifichi la grandezza del tuo codice. Dopo abbiamo le generic function begin() e end() che sono anche loro una nuova funzionalità di C++ 11. Dopo viene la funzione lambda che viene utilizzata come comparatore per i tuoi dati. I parametri sono dichiarati come auto, funzionalità aggiunta in C++ 14. Prima di questa versione, non potevamo utilizzarlo nell’intestazione dei metodi.

Hai notato le parentesi quadre prima dell’inizializzazione della funzione lambda? Definiscono il suo scope: il livello di autorità che la funzione ha sulle variabili locali e gli oggetti.

Come definito in questo dettagliato repository di Anthony Calandra sul moderno C++:

  • []: non cattura nulla. Dunque non è possibile usare nessuna variabile locale dello scope esterno all’interno della tua espressione lambda. Puoi utilizzare solo i suoi parametri.
  • [=]: cattura gli oggetti locali (variabili e parametri) nello scope tramite il valore. Puoi utilizzarli ma non puoi modificarli.
  • [&]: cattura gli oggetti locali (variabili e parametri) nello scope tramite il riferimento. Puoi modificarli come l’esempio in basso.
  • [this]: cattura il puntatore this.
  • [a, &b]: cattura il valore di a ed il puntatore di b.

Dunque, se dentro la tua funzione lambda, vuoi trasformare i tuoi dati in altri formati, puoi usarla traendo vantaggio dallo scope. Per esempio:

Nell’esempio in alto, se hai catturato le variabili locali tramite il loro valore ([factor]), nell’espressione lambda, non puoi cambiare il valore di factor nella riga 5. Perché semplicemente non hai i permessi. 😉

Infine, nota come abbiamo preso val come riferimento. Questo ci assicura che ogni cambiamento nella funzione lambda andrà a modificare la variabile vector.

Gli “statement init” all’interno di if & switch

Ho subito apprezzato questa nuova funzionalità di C++ 11 dopo che l’ho vista.

Apparentemente, ora puoi inizializzare variabili ed effettuare condizioni su esse simultaneamente, all’interno dei blocchi if/switch. È molto importante che tu mantenga il tuo codice conciso e pulito. Per esempio:

if( init-statement(x); condition(x)) {
    // do some stuff here
} else {
    // else has the scope of x
    // do some other stuff
}

Fallo al momento della compilazione con constexpr

constexpr è figo!

Dì che hai alcune espressioni da valutare ed il loro valore non cambierà una volta inizializzate. Puoi pre-calcolare il valore e dopo utilizzarlo come costante. O come offre C++ 11, puoi utilizzare constexpr.

I programmatori tendono a ridurre i tempi di esecuzione dei loro software il più possibile. Dunque se ci sono operazioni che puoi far fare al compilatore, falle fare a lui.


Il codice sopra è un comune esempio di utilizzo di constexpr.

Da quando la funzione di calcolo della sequenza di fibonacci è stata dichiarata come constexpr, il compilatore puoi effettuare il pre-calcolo di fib(20) al momento della compilazione e risparmiare i tempi di esecuzione a runtime.

Dopo la compilazione il valore passerà da così:

const long long bigval = fib(20);

a così:

const long long bigval = 2432902008176640000;

Nota che l’argomento passato è un valore costante (const). Questo è un passaggio fondamentale delle funzioni dichiarate come constexpr. L’argomento passato deve a sua volta essere un constexpr o un const. Altrimenti, la funzione si comporterà normalmente ed il calcolo verrà eseguito a livello runtime.

Anche le variabili possono essere di tipo constexpr. In quel caso, come puoi già dedurre, le suddette variabili saranno eseguite e ne verrà estratto il valore direttamente in compilazione. Tuttavia potresti ricevere qualche errore se non le usi correttamente.

Cosa molto interessante che non ho ancora approfondito e che è stata introdotta nella versione 17: constexpr-if e constexpr-lamda.

Tuple

Allo stesso modo della direttiva pair, le tuple sono collezioni di valori dalla dimensione prefissata di diversi tipi.

Quaalche volta è più conveniente utilizzare std::array invece di tuple. La direttiva array è simile all’array di C insieme ad alcune funzionalità della libreria standard di C++. Questa struttura dati è stata introdotta in C++ 11.

Class template argument deduction

Un nome abbastanza articolato per una funzionalità. L’idea è che, dalla versione 17, l’argument deduction venisse applicato anche ai template di classe standard di C++. Precedentemente era supportato solo dalle template di funzioni.


Come risultato:

std::pair<std::string, int> user = {"M", 25}; // versione precedente
std::pair user = {"M", 25}; // C++17

La deduzione del tipo è fatta implicitamente. Questo diventa molto più conveniente quando utilizziamo le tuple.

// precedente
std::tuple<std::string, std::string, int> user ("M", "Chy", 25);
// deduzione in azione!
std::tuple user2("M", "Chy", 25);

Questa funzionalità non ha alcun senso se non sei familiare con i template di C++.

Puntatori “smart”

I puntatori possono essere infernali.

A causa della libertà che linguaggi come C ++ forniscono ai programmatori, a volte diventa molto facile imbattersi in errori. E in molti casi, i puntatori ne sono responsabili.

Fortunatamente, C ++ 11 ha introdotto puntatori intelligenti, indicatori che sono molto più convenienti dei puntatori grezzi. Aiutano i programmatori a prevenire perdite di memoria liberandola quando possibile. Offrono anche una sicurezza eccezionale.

È tutto per oggi. Ricorda che C ++ ha effettivamente aggiunto molte più nuove funzionalità nelle ultime versioni. Dovresti approfondire l’argomento se sei interessato.

Alla prossima! 😀

Leave a Comment

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *