programming c++ sviluppo codice programmazione coding stile style

C++ al passo coi tempi

Perché per essere eleganti è bene seguire un po' la moda

È una delle cose che più mi fa irritare quando programmo: aprire file cpp e trovar scritto C o una qualche (molto) vecchia versione del C++.

Siamo nel DUEMILAQUATTORDICI e personalmente mi sento un inetto ogni volta che mi rendo conto che c'è una funzionalità del C++11 che se avessi scoperto prima mi avrebbe permesso di scrivere codice più elegante ed efficiente. Ma pare che molti programmatori non si facciano questi problemi, dimenticandosi che esistono cose ormai entrate nello standard almeno dal C++03 (11 anni fa!) o comunque talmente utilizzate da potersi considerare tali. Probabilmente questi tizi girano ancora con le spalline nella giacca e gli occhialoni di tartaruga.

Possiamo avere a nostro vantaggio opere d'arte come la standard library e boost. Non sono strettamente cplusplus, ma è praticamente impensabile non utilizzarle. Infatti la prima è entrata nello standard del linguaggio, la seconda ha influenzato quello che è diventato lo standard C++11.

char* VS std::string

Sembra incredibile, ma continuo a vedere char* usati come si faceva vent'anni fa. Ci sono dei casi estremi in cui è necessario usare un puntatore a char per maneggiare una stringa. Capita di usare librerie in C, che giustamente hanno le API in C. Non ci si può far niente, ci si adatta e si usa il char*. Ma deve essere limitato esclusivamente in quella riga di codice in cui si ha a che fare con il C.

Mettiamo di aver a che fare con una libreria di terze parti per il controllo di un mostro meccanico che ci fornisce solo una API in C:

monster* monster_create();
void monster_get_name(monster* monster, char* name);
void monster_set_name(monster* monster, char* name);

Siamo costretti ad usare quella funzione. Possiamo fare così:

monster* dragon = monster_create();
char name[MONSTER_MAX_NAME_LEN];
monster_get_name(dragon, name);

Addirittura si potrebbe scrivere direttamente in una std::string, visto che solitamente i dati delle stringhe sono contigui. Purtroppo prima del C++11 lo standard non lo garantiva, quindi possiamo scrivere nel seguente modo solo se siamo sicuri di compilare con C++11 o superiore:

std::string name(MONSTER_MAX_NAME_LEN, 0);
monster_get_name(dragon, &name[0]);

A dir la verità, non è poi sta gran differenza, ma se successivamente usiamo name come si dovrebbe (alla maniera C++) evitiamo di fare inutili conversioni.

Se invece dobbiamo passare la string monster_set_name dobbiamo trasformarla in char*:

std::string newName = "Ettore";
monster_set_name(dragon, const_cast<char*>(newName.c_str()));

Perché quel brutto const_cast? Perché le API di monster accettano solo un char* anche se const è stato introdotto nel C nel 1989. Ma visto che non possiamo cambiare le API, ci limitiamo a usare questo trucchetto: funziona ed è limitato ad una sola riga di codice.

E perché ho spiegato questi trucchetti? Perché non usandoli rischiamo che un banale char* che potrebbe rimanere relegato in una riga dilaghi come un virus all'interno del nostro programma o della nostra libreria. Come? Vediamo questo esempio:

/* A modern C++ Object Oriented Monster */
class Monster {
public:
  Monster(char* name) :
          name_(name) {
    wrapped_ = monster_create();
    monster_set_name(wrapped_, name);
  }
private:
  const std::string name_;
  monster* wrapped_;
};

È un esempio di wrapper per trasformare le API originali in C++. Il membro wrapped_ è un'istanza creata dalla libreria esterna direttamente nel costruttore.

Ma c'è una cosa subdola che ci deve far riflettere: chi ha scritto questa classe, per pigrizia ha usato char* anche nel parametro del costruttore. Sempre per pigrizia lo userà anche quando chiamerà new Monster(name) o addirittura scriverà new Monster("Ettore"), a cui giustamente il compilatore risponderà indispettito con warning: deprecated conversion from string constant to ‘char*’.

È questo che intendo quando dico che se non fermato subito, il char* si espande come un virus. Purtroppo ne ho visto fin troppi di questi esempi. Molto meglio bloccarlo sul nascere facendo così:

class Monster {
public:
  Monster(const std::string& name) :
          name_(name) {
    wrapped_ = monster_create();
    monster_set_name(wrapped_, const_cast<char*>(name.c_str()));
  }
private:
  const std::string name_;
  monster* wrapped_;
};

[] vs std::vector

Lo stesso approccio si può usare per i vettori:

monster* children[] = { 
  monster_create(), 
  monster_create(), 
  monster_create()
};
monster_set_children(dragon, children, 3);

Può essere riscritto usando std::vector:

std::vector<monster*> children;
children.push_back(monster_create());
children.push_back(monster_create());
children.push_back(monster_create());
monster_set_children(dragon, children.data(), children.size());

Così evitiamo il magic number e il codice risulta più chiaro. Ovviamente le istanze non verranno distrutte quando finisce lo scope del vettore, perché è un semplice vettore di puntatori.

Object Oriented

Anche in C si può scrivere ad oggetti (l'esempio di prima lo era), ma il C++ è Object Oriented e permette di farlo meglio. Facciamolo! È inutile usare lunghe liste di funzioni globali quando possiamo pensare l'intero design del progetto a oggetti. Raramente la ricerca dell'efficienza basta a scusare approcci old-style, visto che i moderni compilatori sono più bravi di noi a ottimizzare il codice.

Definire e inizializzare

Vecchie (molto vecchie!) versioni di C e C++ richiedevano di definire le variabili all'inizio dello scope. Son passati anni da quando standard e compilatori permettono di definirle in qualsiasi punto, ma ancora troppi developer continuano a metterle all'inizio della funzione. A che scopo? Sono variabili locali, agli sviluppatori "clienti" (quelli che leggono l'interfaccia pubblica) a nulla serve vedere le variabili locali tutte belle ordinate all'inizio. Anzi, nemmeno dovrebbero guardarla l'implementazione. E i programmatori che invece ci mettono mani fanno fatica a capire quando effettivamente una variabile entra in gioco. E poi... non dimentichiamo l'incubo delle variabili non inizializzate! Se le definiamo quando ancora non abbiamo abbastanza elementi per inizializzarle, rischiamo di lasciare del codice potenzialmente pericoloso. La pericolosità è subdola, perché può essere che per anni una variabile non inizializzata abbia "fatalità" (ma non è raro) il valore aspettato e tutt'ad un tratto (usando un nuovo compilatore, inserendo una variabile da tutt'altra parte, cambiando architettura...) tutt'altro valore compare a quel posto. Purtroppo non è un caso così remoto. Il fatto è che le variabili non inizializzate possono avere qualsiasi valore casuale, ma spesso hanno valore zero ed è difficile accorgersene subito. Per questo motivo:

Definire le variabili locali il più localmente possibile e solo quando ci sono abbastanza informazioni per inizializzarle!

Evita di gonfiare l'ambito (scope) delle variabili: esse introducono stati, e dovresti maneggiare meno stati possibili, con una vita il più corta possibile. Andrei Alexandrescu

Ti è piaciuto l'articolo? Condividilo! Commentalo!

comments powered by Disqus