Uno dei motivi per cui ritengo C++ uno dei migliori strumenti per lo sviluppo software è la possibilità che offre di realizzare un design accurato dei componenti di un sistema, così come di veicolare le intenzioni dello sviluppatore, senza per questo sacrificare le performance del prodotto finale. E non parlo solo del fatto che in C++ (a differenza di ANSI-C) è possibile definire classi personalizzate. Intendo tutta quella serie di interazioni che nascono fra componenti dello stesso binario, che costituiscono il contratto stipulato tra il designer di un modulo e i suoi clienti.

C++ Core Guidelines è un testo coredatto da Bjarne Stroustrup (hint: il creatore del linguaggio) per cercare di riassumere in un unico punto delle linee guida per scrivere codice C++ moderno, manutenibile e comunicativo delle intenzioni dello sviluppatore. In questo post vorrei porre l'attenzione sul capitolo relativo a Costanti e Immutabilità, aggiungendo alcune mie impressioni ed esperienze maturate in questi anni di utilizzo del linguaggio.

Perchè?

Il concetto di immutabilità è uno strumento utilissimo che ci permette di specificare che un oggetto, istanziato e inizializzato in questo punto del codice non cambierà più il suo stato. La possibilità di dichiarare un oggetto x immutabile informa il lettore del codice circa le intenzioni del suo ideatore, ovvero quelle di porre il valore di x come invariante del corpo di una certa funzione, di un'intera classe o di un'intero programma. Inoltre, un uso sistematico di oggetti immutabili porta a codice più comprensibile (riduzione del cognitive load, ne parliamo più avanti), a scoprire bug subito alla compilazione piuttosto che dopo un estenuante debugging, e certe volte anche a performance migliori (specie in programmi multi-thread).

Come abitudine, definisci oggetti immutabili

La prima linea guida riguarda la definizione di variabili nel corpo delle funzioni che scriviamo. Nel dubbio, dichiariamole const.

Questo semplice accorgimento è quello che nella pratica mi ha dato maggiori soddisfazioni. In primis, una variabile dichiarata const ha un impatto impressionante sulla leggibilità del codice. In Computer Science, questo fenomeno è chiamato riduzione del cognitive load. Nel processo di comprensione di una porzione di codice (tipicamente privo di commenti utili e nomi di variabili auto-esplicativi), quando si incontra un oggetto x dichiarato const, il cervello può immediatamente rimuovere l'attenzione da quel simbolo, smettendo di seguirne l'evoluzione. Ciò consente di portare il focus sui soli elementi variabili, riducendo lo sforzo necessario per decifrare l'intento dello sviluppatore.

Anche altri linguaggi hanno la possibilità di dichiarare oggetti più o meno costanti. Ad esempio, Java mette a disposizione la keyword final per questo. Ma C++ fa molto di più. In Java infatti una variabile x dichiarata final sta a significare che x, una volta inizializzato, non sarà riassegnato a nessun altro oggetto. Tuttavia, su un oggetto dichiarato final potremo ancora invocare qualunque metodo esso esponga, anche quelli che ne modificano lo stato. Un oggetto dichiarato const in C++, d'altro canto, permette solo l'invocazioni di metodi a loro volta dichiarati const; ciò significa che nessuna linea di codice successiva alla sua dichiarazione potrà alterarne lo stato. Questo ci porta al secondo vantaggio della dichiarazione delle variabili const: errori in compilazione.

I bug, si sa, sono sempre dietro l'angolo. Per quanto uno sviluppatore si sforzi, scrivere codice bug-free è un'utopia, pertanto è saggio servirsi di tutti i tool a nostra disposizione per evitare noiose sessioni di debugging per poi accorgersi di aver assegnato un valore alla variabile sbagliata.

Definendo una variabile const, comunichiamo al compilatore la nostra intenzione di tenere quella variabile immutabile, inalterabile. Se per errore quindi ci trovassimo ad assegnarla nuovamente, a muoverla con il costrutto std::move o ad invocare qualche metodo non-const, il risultato sarà un chiaro (si spera) errore in fase di compilazione, che ci avviserà che stiamo provando a modificare una variabile dichiarata costante. Solo dopo aver passato ore nel debugger (perché non usiamo le printf per debuggare vero?!) potremo capire quanto importante è avere un compilatore che ci avvisi di aver tradito le nostre originali intenzioni. Da queste osservazioni pratiche, deriva la seconda linea guida.

Come abitudine, dichiara metodi const

Per quello che ci siamo detti qui sopra, avere oggetti immutabili rende il codice estremamente più leggibile e meno prono all'errore. Inoltre, il compilatore sarà nostro amico nel rifiutare di compilare un programma che presenti variabili dichiarate const su cui si invochino metodi non-const. Pertanto, se stai scrivendo una classe, abituati a dichiarare i suoi metodi const a meno che non alteri lo stato osservabile dell'oggetto. Sei sempre in tempo a tornare indietro!

Questo pezzo di codice ci permette di fare alcune osservazioni. In primis, notiamo la presenza di un metodo accessore il cui scopo è quello di esporre lo stato dell'oggetto Point. Ci si aspetta quindi che invocare questo metodo non alteri in alcun modo lo stato osservabile dell'oggetto. Successive chiamate a getx() dovrebbero ritornare sempre lo stesso valore di x. Tuttavia il designer della classe non l'ha marcato const. Qual è il risultato? Che tutto il resto del codice che desideri utilizzare un'istanza const di Point non potrebbe invocare getx() e leggere lo stato dell'oggetto perché otterrebbe un errore in compilazione.

Leggendo penserai: "Beh che problema c'è? Dichiaro getx() come const!". E avresti ragione. Peccato che tipicamente ciò non accade o potrebbe anche non essere possibile. Non accade perché l'approccio standard di uno sviluppatore è arrivare il più in fretta possibile ad un programma compilato con successo, preferendo rimuovere attente scelte di design per minimizzare il numero di errori in compilazione. Quindi, armato di CTRL+F, andrebbe alla ricerca di tutti i const Point per rimuoverne l'immutabilità. Scelta pessima.

Ma anche qualora questo non fosse il tuo caso, potrebbe non esserti proprio possibile agire sulla classe! Se infatti questo problema si presentasse in una libreria di terze parti, a cui tu hai accesso solo mediante dei pre-compilati, non avresti alcuna possibilità di mettere le mani al codice. Dovresti aprire una issue allo sviluppatore della libreria nella speranza che risolva al più presto.

In secondo luogo, un metodo const offre una chiara garanzia nel contratto stipulato tra il designer della classe e i suoi clienti. Anche qualora la documentazione del metodo fosse scarsa, il cliente, leggendo la signature del metodo, può concludere con certezza che l'unico effetto di quel metodo è fornire una vista sullo stato dell'oggetto. Punto.

Cosa succede invece se, incappando nel metodo getx(), vediamo che è dichiarato non-const? Nella nostra testa dovrebbero scatenarsi tutta una serie di domande. "Ma questo metodo è un accessore o un modificatore? Se lo chiamo più volte il risultato è lo stesso? Ma sì dai, si inizia con get, di solito lo si usa per leggere lo stato. Però... metti che poi fa qualcosa che non mi aspetto e lo sviluppatore si è dimenticato di scriverlo della documentazione... Aspetta che mi leggo il codice va..."

Sono certo che un dialogo interiore come questo è capitato a tutti. Ritorniamo così al punto di partenza. Dichiarare oggetti e metodi const ha un'impatto drammatico sulla leggibilità del codice.

Conclusione

In questo post, ho riportato la mia esperienza riguardo all'immutabilità nello sviluppo in C++ partendo dalle prime due linee guida del C++ Core Guidelines. Sono piccoli accorgimenti, che richiedono una certa disciplina nello scrivere codice, ma che mi hanno dato grandi soddisfazioni. L'impatto è immediato e sarà di immenso aiuto a colleghi, clienti e noi in prima persona, quando rileggeremo il nostro codice nei mesi successivi.

Cominciamo oggi a spargere un po' di constness! Buon lavoro! 😉