programming c++ sviluppo codice programmazione coding stile style

Lo sviluppatore gentiluomo

Appunti di stile, buone maniere e quello che un programmatore C++ dovrebbe evitare

Questo articolo nasce da uno sfogo, dopo aver visto per l'ennesima volta un abominio che difficilmente si può considerare un programma e tantomeno codice C++.

È uno sfogo, ma voglio che sia costruttivo, una serie di consigli per quei programmatori alle prime armi, o quei veterani che nonostante tutto non hanno ancora capito un cazzo.

Non è l'ennesimo manuale di programmazione, ce ne sono di ottimi, ma qualcosa di meno tecnico... più estetico. Perché il contenuto non è tutto quando si scrive un buon programma. Il fatto che un'applicazione funzioni e faccia il suo dovere, non deve essere sufficiente perché si possa ritenere benfatta.

Infatti quello che voglio trattare è una questione di stile, di buone maniere, di norme che i gentiluomini del codice dovrebbero seguire, soprattutto quando lavorano in team.

Ho iniziato a programmare quando ero ragazzino e schifezze incomprensibili ne ho viste (e talvolta scritte) in tutti i linguaggi, soprattutto nel javascript, forse il codice più violentato di tutti i tempi. È spesso il primo a cui i principianti si rivolgono per imparare qualcosa, perché perdona tante cose. Ha le sue pecche, ma anche i suoi punti di forza. Ho letto capolavori scritti in js, perché quando lo si comprende bene è potente ed espressivo. Rimane il fatto che la maggior parte siano orrori, ma di solito perdòno gli autori perché sono probabilmente alle prime armi.

Quelli che non posso perdonare sono i disgraziati che scrivono male in C++. È un linguaggio creato a tavolino, con regole molto precise e che perdonano poco, continuamente in evoluzione da molti anni. Forse prolisso, ma con un ricco vocabolario (quasi un centinaio di keywords), è l'erede del C, ma corretto e potenziato, soprattutto per la programmazione ad oggetti. È bello da leggere, se scritto bene.

È infatti dell'estetica che voglio trattare, perché un programma bello da leggere è più semplice da mantenere e meno prono agli errori.

No comment!

Quando scriviamo un programma, in qualunque linguaggio formale, dobbiamo perseguire due forme di comunicazione: con la macchina, attraverso il compilatore, e con altri programmatori (inclusi noi stessi), che in seguito avranno il compito di comprendere, modificare, o riutilizzare il nostro codice. C++ Manuale di stile Reloaded, Carlo Pescio

Trovo opportuno partire da un principio da cui trarre tutte le conseguenti regole:

scrivi sempre il codice come se non fosse possibile aggiungere commenti

A cui aggiungerei una cosa non ovvia per tutti:

...e lo dovesse leggere un'altra persona

Aggiungo questo perché, per quanto tu possa essere convinto che sarai l'unico a mettere mano al tuo progetto topsecret, prima o poi qualcun altro dovrà averci a che fare. E quel qualcuno potresti essere tu nel futuro, e bestemmierai nel cercare di capire cosa intendevi scrivere in quelle assurde righe anni e anni addietro.

Perché evitare i commenti? Il commento nel codice è uno strumento importante, ma va usato nel modo giusto. Innanzitutto non va commentato tutto come se si dovesse spiegare alla maestrina perché si son fatti certi passaggi. Visto che il C++ (come la maggior parte dei linguaggi) ci permette di scrivere i nomi delle variabili con una certa libertà, e le keywords sono praticamente tutte parole inglesi, possiamo strutturare i nostri programmi in modo da poter essere letti quasi come fossero in inglese.

Ad esempio:

for (;;) {
  std::cout << "Hello" << std::endl;
}

Può essere riscritto con

while (true) {
  print("Hello");
}

Mentre la prima si legge più o meno "for semicolon semicolon std cout hello std endl", che in inglese non vuol dire proprio niente, la seconda, già più bella e pulita per l'occhio, si legge "while true print hello", che ha lo stesso significato del codice: finché vero è vero, stampa "Hello". Ovviamente va definita la funzione print se già non esiste.

Non è un caso che abbia creato una funzione chiamata print, che è un verbo. Le funzioni sono serie di azioni finalizzate a uno scopo, ed è quello scopo che deve dare il nome all funzione. E sarà sempre un verbo o addirittura una breve frase, ma comunque con un verbo in mezzo. Stiamo parlando di C++, un linguaggio ad oggetti, quindi è più probabile che ci troviamo a scrivere dei metodi nelle classi piuttosto che mere procedure. Il discorso non cambia, sempre di azioni si tratta, e normalmente l'istanza è il soggetto del verbo (talvolta l'oggetto). Se non hai capito questa frase, forse a questo punto ti serve un ripassino di grammatica, quella che si studia alle elementari.

Funzioni con side-effects (procedure) devono avere un nome che descriva ad un giusto livello di astrazione tutti i compiti eseguiti. Usate un verbo seguito da un complemento oggetto per le funzioni non membro, ed un verbo per le funzioni membro, lasciando l’oggetto implicito. C++ Manuale di stile Reloaded, Carlo Pescio

class Body {
public:
  Body();
  virtual ~Body();
  void rotate(Radian angle);
  void applyForce(Force force);
  void setPosition(Position position);
  bool isMoving();
  Position getPosition();
  Geometry getGeometry();
private:
  Mass const mass_;
  Geometry geometry_;
  Speed speed_;
  Position position_;
};

La classe Body ha dei metodi, i cui nomi sono verbi o composizioni. Il primo è un semplice verbo (rotate). Il secondo è un verbo seguito da un sostantivo che esplicita cosa si sta applicando (applyForce). Il terzo è un metodo che imposta una proprietà, detto anche setter (setPosition). Il quarto è un'interrogazione (isMoving). Il quinto e il sesto danno il valore di una proprietà, detti anche getter. Il verbo c'è in tutti: rotate, apply, set, is, get.

In questo modo possiamo istanziare un oggetto e continuare a scrivere in pseudo-inglese, usando appunto i verbi. Nell'esempio successivo è chiaro cosa sta succedendo, tanto che un commento non farebbe altro che ripetere quello che già si evince leggendo a voce il codice. Suvvia... se non conosci l'inglese è più difficile. Ma veramente pensi di intraprendere una carriera di programmatore senza conoscere un po' di inglese?

Body ball;
ball.rotate(PI / 2.0);
ball.applyForce(INITIAL_FORCE);

Addirittura, se usato con le istruzioni condizionali, tutto fila liscio. Il prossimo esempio si legge proprio "if ball is moving, ball set position to default position.". Wow! Magari non è proprio come parlano a Oxford, ma anche uno che non ha mai letto del codice riesce a capire cosa stia succedendo!

if (ball.isMoving()) {
  ball.setPosition(DEFAULT_POSITION);
} 

La lingua

Il c++, come la maggior parte dei linguaggi ad alto livello, parla inglese americano. Le keywords sono in inglese e alcuni compilatori non riescono a gestire le lettere accentate dell'italiano, nemmeno nei commenti. Comunque, queste lettere sono illegali nel codice. Quindi: evitare l'italiano! Il codice si scrive in inglese, come i nomi delle variabili, delle funzioni etc... Se proprio si è negati, i commenti si possono scrivere in italiano. Ammetto che spesso mi trovo a scrivere i commenti in italiano, a volte per fretta, altre volte perché lavoro in team solo con italiani e quindi ci si trova meglio. In quei casi, comunque, niente lettere accentate, anche se il compilatore le capisce, piuttosto uso il backquote `.

E i commenti? Se utilizziamo le tecniche scritte sopra, ci sarà poco bisogno di commentare, perché il codice è già chiaro di suo.

Immutabilità esplicitata

Riprendo l'esempio di prima e aggiungo qualche const in giro...

class Body {
public:
  Body();
  virtual ~Body();
  void rotate(const Radian& angle);
  void applyForce(const Force& force);
  void setPosition(const Position& position);
  bool isMoving() const;
  Position getPosition() const;
  const Geometry& getGeometry() const;
  Vector getLastTranslation() const;
private:
  Mass const mass_;
  Geometry geometry_;
  Speed speed_;
  Position position_;
  Position oldPosition_;
};

La classe si usa nella stessa maniera e il significato originario rimane, ma è arricchito da qualche informazione in più. E non sono serviti i commenti.

La keywords const non è messa a caso e ha almeno due motivi di esserci:

  • indica a chi legge il codice che il valore non cambia
  • in fase di compilazione genera errori se i metodi vengono usati "male"

In realtà, a seconda della posizione ha significati diversi, ma questo argomento si può approfondire su un manuale di C++. Quello che voglio sottolineare è che anche se la keyword non avesse alcun significato per il compilatore, lo avrebbe comunque per il programmatore:

  • se è un parametro, siamo sicuri che l'istanza passata come parametro non viene modificata.
  • se è una proprietà, siamo sicuri che non cambierà di valore dopo l'inizializzazione.
  • se è alla fine della dichiarazione di un metodo, siamo sicuri che quel metodo non modificherà le proprietà dell'istanza.**
  • se è l'output di un metodo, siamo sicuri che chi accede alla reference non può modificarne il valore, ma solo leggerlo.
Attenzione! Non è proprio esatto che siamo sicuri che il valore rimanga costante, ci sono dei modi per bypassare la constantness, per esempio con const_cast o la keyword mutable. Ma ne tratterò più avanti, perché è un'esempio di quello che farebbe uno zoticone, non un gentiluomo.

Da notare anche l'aggiunta della & nei parametri e nell'output, perché usando const possiamo tranquillamente passare gli oggetti per reference senza preoccuparci che vengano sbadatamente modificati. Così evitiamo pure di fare inutili copie.

Attenzione! Un programmatore zoticone potrebbe sempre bypassare il problema, usando il maledetto const_cast sull'output del getter, ma questa non è una sbadataggine. È solo una cafonaggine.

Da notare la differenza fra i due getter:

  Position getPosition() const;
  const Geometry& getGeometry() const;

Il primo restituisce una copia (e assicura di non fare modifiche all'istanza), il secondo restituisce un alias immutabile. Avremmo potuto anche scrivere

  const Position& getPosition() const;
  const Geometry& getGeometry() const;

...dato che position_ è una proprietà dell'oggetto. Nel caso invece venga restituito qualcosa di calcolato al volo, è impossibile restituirlo per reference, ma c'è bisogno di una copia. È l'esempio di getLastTranslation() che restituisce la differenza fra position_ e oldPosition_. È uno dei motivi per cui molti programmatori preferiscono comunque restituire sempre "per valore", piuttosto che per reference, forti del fatto che il compilatore ottimizzerà il codice evitando di fare inutili copie.

Quindi, anche se questo potrebbe avere un impatto sulle prestazioni del programma, soprattutto se l'oggetto restituito occupa molta memoria, i benefici sono molti.

Vediamo l'esempio:

  Position getPosition() const;
  Geometry getGeometry() const;

Non abbiamo bisogno di specificare const sull'oggetto restituito (anzi, alcuni compilatori danno un warning se lo facciamo) proprio perché quello che viene restituito è una copia. Innanzitutto ci assicuriamo che l'altro programmatore (zoticone) che userà la nostra classe non riuscirà a modificare un membro privato del nostro oggetto, per esempio con il const_cast. Poi, se un bel giorno vorremmo modificare il getter in modo da restituire qualcosa di creato al volo anziché un membro, allora non saremo costretti a modificare l'interfaccia.

Ma torniamo al const...

Avere questa keyword, oltre a dare benefici al programmatore che leggendo il codice riesce subito a capire cosa può fare e cosa non può accadere, implica però altre cose: il compilatore diventa meno permissivo e probabilmente riesce ad ottimizzare anche il codice. Ma non siamo qui a parlare di ottimizzazione...

Una compilazione meno permissiva, aiuta a gestire certi errori e limitare i campi di possibilità di scrivere codice bacato: se provo a scrivere su una variabile che non dovrebbe cambiare, me ne accorgo già in fase di building, perché il compilatore fallisce.

I tipi da non sottovalutare

Tornando all'estetica... negli esempi ho preferito usare dei type ben definiti (Radian, Force, Position, Speed...) per dare subito un'idea di cosa stiamo trattando. Radian potrebbe essere semplicemente un alias di float, ma almeno vediamo a colpo d'occhio che stiamo trattando di radianti e non di gradi. In questo modo possiamo evitare di scrivere un commento sopra ogni funzione che ricordi al programmatore che si sta parlando di radianti. Stesso discorso per Speed e Position, che potrebbero essere entrambi alias di una fantomatica classe Vector3, ad esempio.

L'alias si definisce semplicemente con:

typedef float Radian;
typedef float Real;
typedef int Integer;

Perchè la prima lettera maiuscola? Per distinguere meglio i tipi (e le classi) dalle variabili. Io di solito adotto uno standard abbastanza comune:

  • camelCase con prima lettera minuscola per funzioni e variabili
  • CamelCase con prima lettera maiuscola per tipi e classi
  • SNAKE_CASE tutto maiuscolo per costanti statiche e macro

Sarà perché ho imparato a programmare in C++ usando la libreria Qt, ma personalmente preferisco questo tipo di notazione. Lo ammetto, è una scelta personale, notazioni diverse hanno altri pro e contro. L'importante, come sempre, è essere omogenei.

Anche se entrambe camelCase con la prima minuscola, la differenza fra funzione() e variabile, si nota comunque dalle parentesi alla fine. Non mi piace fare differenza fra tipi primitivi, classi e strutture, perché sempre di "tipi" si tratta. Così preferisco sempre lavorare con tipi e classi con la prima lettera maiuscola, tanto che mi capita spesso di creare l'alias Real al posto di float, così da avere un codice più omoegeneo.

L'ordine delle cose

Arricchisco ulteriormente l'esempio, inserendo costanti, definizioni di tipi e metodi di classe.

class Body {
public:
  typedef std::queue<Vector*> PositionHistory;
  static void setDefaultSpeed(const Speed&);
  static void setDefaultMass(const Speed&);
  Body();
  Body(const Speed& speed, const Mass& mass, const Geometry& geometry);
  Body(const Body& body);
  virtual ~Body();
  const Geometry& getGeometry() const;
  bool isMoving() const;
  Position getPosition() const;
  void rotate(const Radian& angle);
  void applyForce(const Force& force);
  void setPosition(const Position& position);
protected:
  Geometry* geometry();
private:
  enum Status {
    STATUS_ACTIVE,
    STATUS_IDLE
  };
  typedef std::list<Body*> Instances;
  static unsigned getInstancesCount();
  static Instances instances_c;
  Vector calculateDirection() const;
  Mass const mass_;
  const Geometry* geometry_;
  Speed speed_;
  Position position_;
  Status status_;
  PositionHistory positionHistory_;
};

L'esempio ora è complesso, probabilmente non scriverei mai una dichiarazione così lunga, ma è utile per vedere i vari casi e come li ho ordinati.

Adotto vari modi di ordinare le righe nella dichiarazione e ognuno di questi "lavora assieme" agli altri.

C'è l'ordine per protezione, dal meno protetto in alto (public) al più, in basso, protetto (private). Ordinati in questo modo perché allo sviluppatore che va a leggersi le API interessa l'interfaccia pubblica prima di tutto, forse quella protetta, sicuramente non quella privata, che infatti viene lasciata per ultima (se si potesse nascondere del tutto sarebbe meglio).

C'è l'ordine per classe/oggetto. Proprietà e metodi di classe (quelli che iniziano con static) sono per primi perché influenzano l'intera classe e di conseguenza anche le istanze. Proprietà e metodi di oggetto vengono dopo, perché specifici all'istanza relativa.

C'è l'ordine per tipo/funzione/proprietà. Dovevo dare un'ordine, l'ho scelto perché mi sembra più bello da leggere, non per particolari motivi. Le proprietà, che dovrebbero essere le cose più protette in assoluto, vanno a finire nella parte più bassa in assoluto. Quindi metto sempre i typedef e gli enum prima delle funzioni, quindi a seguire le proprietà.

C'è l'ordine di const dove gli immutabili vengono prima. Perché mi piace così.

A colpo d'occhio

Sempre per il vecchio motivo, cioè che vogliamo minimizzare l'uso di commenti, aggiungiamo ai nomi dei complementi per far meglio capire di cosa si tratta.

Come già detto prima, le variabili sono in camelCase con la prima lettera minuscola. Possono esserci quindi solo lettere e raramente numeri (comunque tranne il primo carattere). Questa regola vale per tutte le variabili e costanti locali.

Real const scale = 2.0;
size *= scale;

Abbiamo già visto negli esempi le variabili oggetto e quelle di classe, le prime con _ finale, le seconde con _c finale (c sta per class). Questo vale anche per le costanti dell'oggetto.

static Instances instances_c;
Mass const mass_;
Speed speed_;

Per le costanti statiche e di classe (static const), invece usiamo lo SNAKE_CASE_UPPERCASE per un particolare motivo: sono le uniche vere costanti a già in fase di compilazione. Cosa significa? Gli altri casi (costanti locali e costanti di oggetto) sono immutabili solo nel loro dominio (che sia una blocco o un oggetto). Infatti due oggetti Body potrebbero avere una mass_ differente. Le costanti di classe, come le costanti globali, sono invece definite una sola volta all'interno del codice, tanto che alcuni compilatori le trattano come se fossero dei valori letterali.

Globale:

static const PI = 3.14159265359;

Di classe:

class Body {
  static const DEFAULT_POSITION;
  static const DEFAULT_MASS = 1.0;
}

Addirittura, se di tipo numerico o enum, si può inizializzare direttamente nella dichiarazione della classe.

Per lo stesso motivo, scrivo nella stessa notazione anche le enum. In più, per evitare problemi di ambiguità, agli elementi delle enum metto come prefisso il nome della enum, anche quello in UPPERCASE. Per questo diventa:

enum Status {
  STATUS_ACTIVE,
  STATUS_IDLE
};

Anche le costanti per eccellenza, quelle del preprocessore, nello stesso modo:

#define APP_VERSION_MAJOR 1
#define APP_VERSION_MINOR 8
#define APP_VERSION_PATCH 1

Preferisco sempre usare le static const al posto delle macro, ma ci sono dei casi in cui le macro sono necessarie, ma ne parleremo più avanti.

Tornando alle variabili... ci sono dei casi (da evitare come la peste!) in cui ci troviamo a dover usare delle variabili globali. Facciamolo subito ben presente per limitare il dilagare del caos, con un bel suffisso:

int theFuckingGlobalSwitch_G;

Scusa l'imprecazione, ma è esattamente quello che penso delle variabili globali. Nel C++ sono sempre sintomo (e causa) di design sbagliato ed è per questo ci metto quella _G maiuscala alla fine. Ovviamente nel codice reale no vanno messe imprecazioni!

Una grande G di Global, brutta da vedere, dovrebbe subito scoraggiare qualsiasi impavido ad usarla senza le opportune pinze. Non mi vengono in mente dei casi reali in cui sia necessario usare delle variabili globali, se non per usare librerie che lo richiedono, magari perché son scritte in C.

Il più delle volte, queste variabili globali ci sono utili per ottimizzare il codice, quindi solo all'interno di un unico file cpp.

In questi casi si possono usare le globali static, che evitano di inquinare lo spazio dei nomi (external linkage). Con molta fantasia, ma con meno arroganza, può bastare un _g alla fine per segnalarlo:

int lessInvasiveGlobalSwitch_g;

Molto meglio usare il namespace anonimo, magari all'inizio del file, così evitiamo pure l'abusata parola static che in C++ significa fin troppe cose.
Per approfondire
In questo caso, un discreto _u alla fine può bastare a ricordarci che non abbiamo a che fare con una variabile locale e dobbiamo tenere una certa attenzione.

namespace {
  int nobodyCanSeeMeOutside_u;
}

Snello è bello

the best code is no code at all Jeff Atwood

Scrivere in modo conciso non significa usare nomi variabili corti, che anzi peggiorano la lettura. Ma evitare ripetizioni, ghirigori e indentazioni esagerate.

Se ci troviamo di fronte a questo:

player[3].getBody().setPosition(Vector3(x,y)):
player[3].getBody().setSpeed(Vector3(vx,vy)):
player[3].getBody().setStatus(Player::STATUS_ACTIVE):

Possiamo semplificarlo in

Body& body3 = player[3].getBody();
body3.setPosition(Vector3(x,y)):
body3.setSpeed(Vector3(vx,vy)):
body3.setStatus(Player::STATUS_ACTIVE):

Non stiamo nemmeno istanziando un nuovo oggetto, perché così si crea un alias di player[3].getBody(). Mi è capitato di vedere righe che sono al 80% una serie di getter, ripetute svariate volte. È brutto anche perché si superano facilemente gli 80 caratteri per riga e si fatica a leggere. Un esempio è quello che succedo in Ogre3d quando si vuole accedere alla texture di uno SceneNode.

node->getSubEntity(0)->getMaterial()->getTechnique(0)->getPass(0)->getTextureUnitState(0)->setTextureName(config->getBackgroundFilename());

Ovviamente, se usiamo più volte il puntatore a TextureUnitState restituito dal quinto passaggio, è d'obbligo definire all'inizio una variabile a quel puntatore e poi riutilizzarla. Ma visto che la riga è così lunga, è opportuno spezzarla comunque. Può diventare:

MaterialPtr material = node->getSubEntity(0)->getMaterial();
Pass* pass = material->getTechnique(0)->getPass(0);
std::string filename = config->getBackgroundFilename():
pass->getTextureUnitState(0)->setTextureName(filename);

In realtà, dopo questa modifica, il codice è leggermente più lungo, ma la leggibilità è migliorata.

Altra causa delle righe troppo lunghe sono le liste di parametri lunghissime. Il segreto è tenere le funzioni con meno parametri possibili, al massimo 3. Se ci troviamo a scrivere una funzione con più di tre parametri significa che abbiamo sbagliato qualcosa di più profondo, ad esempio lasciandole fare troppe cose. Perché il segreto per una buona funzione è che faccia un unica cosa. Per cui... troppi parametri... sospiro di concentrazione e capiamo perché ne ha bisogno di così tanti. Se non possiamo dividerla in funzioni più semplici e i parametri sono tutti necessari, allora è meglio creare una struttura che li tenga e passare direttamente la struttura.

Ad esempio, il costruttore

Triangle(float x0, float y0, float x1, float y1, float x2, float y2);

diventa

Triangle(const Point& p1, const Point& p2, const Point& p3);

e a Point viene delegato il compito di tenere x e y.

Magro è brutto

Sempre all'erta, un codice troppo magro diventa incomprensibile:

int i = 0, a;
for (;;++i)
  if (!(a = i % 13))
    break;

Mancano le graffe, il for è "incompleto", si assegna e si controlla contemporaneamente un valore. Non capiamo a cosa si riferisca la variabile a. In questi casi verrebbe da scrivere un commento che spieghi passo passo cosa succede. Ma proviamo a riscriverlo in maniera più leggibile...

int remainder = 0;
for (int i = 0; /* ignore */; ++i) {
  remainder = i % 13;
  if (remainder == 0) {
    break;
  }
}

Chiedo venia per l'esempio, che non fa alcunché di utile, ma ci siamo capiti: scrivere qualcosa in più non fa male. Forse i è uno dei pochi casi in cui va bene tenere un nome di variabile corto, perché è prassi comune usare i come iteratore.

L’uso delle variabili i e j come indici dei loop è molto chiaro, e non sarebbe realmente più espressivo utilizzare nomi più lunghi come index.

Per tutto il resto, gli identificatori devono avere un nome chiaro ed espressivo. Nell'esempio il nome remainder fa capire subito di cosa si tratta. È dichiarato prima del for perché il risultato verrà usato successivamente.

Le parentesi graffe, mettiamole! Non è sta gran fatica e diventa più leggibile. Poi evitiamo che accidentalmente succeda questo:

int i = 0, a;
for (;;++i)
  if (!(a = i % 13))
    print(a);
    break;

Che ha tutt'altro significato, visto che il break sta in realtà fuori dall'if (anche se l'indentazione sbagliata lo può far pensare). Inizializziamo una variabile per volta, meglio se appena serve, così da limitarne la visibilità a quello che serve. Evitiamo di controllare un valore nella stessa riga in cui lo si assegna ad una variabile, è facile confondere (a = b) con (a == b). E diamo un nome più chiaro alle variabili.

Repetita NON iuvant

Perché se ripetendo le cose nella vita normale diventiamo sempre più bravi a farle, quandi si tratta di programmare diventa inutile e noioso. Quindi: ripetere non giova.

Ho usato il latino perché fa figo, in inglese è più conosciuta come DRY, Don't Repeat Yourself, che è bello anche perché significa asciutto, che rende bene l'idea.

Se ci troviamo spesso a scrivere le stesse righe di codice, forse è il momento di creare una funzione e richiamarla quando serve.

Se in una classe abbiamo due o più funzioni molto simili, dove l'algoritmo è lo stesso ma cambiano i parametri o le variabili in gioco, allora è il caso di creare una funzione più generica, che verrà richiamata con parametri diversi dalle "specializzazioni".

Esempio banale:

void Car::steerLeft(Radian angle) {
  /* lungo algoritmo che calcola come sterzare la macchina a sinistra
   * ometto il codice, che non serve nell'esempio
   */
}

void Car::steerRight(Radian angle) {
  /* lungo algoritmo che calcola come sterzare la macchina a destra
   * ometto il codice, che non serve nell'esempio
   */
}

Siccome le due funzioni sono praticamente uguali, facciamo in modo di avere l'algoritmo in un solo posto:

void Car::steer(Radian angle) {
  /* lungo algoritmo che calcola come sterzare la macchina
   * ometto il codice, che non serve nell'esempio
   */
}

void Car::steerLeft(Radian angle) {
  /* richiamo steer, invertendo il segno dell'angolo */
  this->steer(-angle);
}

void Car::steerRight(Radian angle) {
  /* richiamo steer */
  this->steer(angle);
}

Qualcuno avrà da ridire che così aggiunto una funzione, ma se implementiamo dei miglioramenti all'algoritmo, lo dobbiamo fare una volta sola, non due.

Sembrerà banale, sarà la prima cosa che ti insegnano quando si inizia a programmare, ma pare che la maggior parte dei programmatori se lo dimentichino spesso. Oppure le teste di cazzo le ho trovate tutte io.

In C++ abbiamo pure l'ereditarietà, che è una forte arma contro la ripetizione. Quando abbiamo classi (es: Apple, Peach, Tomato) con elementi simili, possiamo delegare questi elementi (che siano metodi o proprietà) ad un'altra classe (es: Fruit) che quelle classi erediteranno.

Ma gli esempi non finiscono qui e sono talmente tanti che non li posso trattare tutti.

La regola base è: non ripetere e cerca di trovare gli elementi in comune per generalizzare.

Il codice diventerà più snello e mantenibile.

Divide et impera

Perché tutti son bravi a scrivere funzioni di centinaia di righe, ma chi le legge bestemmia sanguinando agli occhi.

Se programmando ci troviamo ad usare insistentemente la rotellina del mouse, le freccine o il PagUp/PagDown della tastiera, vuol dire che lo stiamo facendo sbagliato.

Tutto ciò che è lungo, è difficilmente leggibile, poco mantenibile e attira un sacco di bachi. Un po' come le case di quei pazzi che compulsivamente si riempiono di cose inutili. Dopo un po' ci fai fatica a camminare, cominciano a nidificarci insetti e ratti e il casino diventa tale che ti vien voglia di dar fuoco a tutto.

Ecco, quando trovo un sorgente con più di 400 righe mi vien voglia di dargli fuoco (assieme al programmatore).

Le funzioni lunghe sono le più pericolose, perché tendono a diventarlo sempre di più. Dividendole in funzioni più corte, cercando di rimanere sotto le 10 righe (commenti esclusi) riusciamo a darci un ordine mentale migliore. E spesso ci accorgiamo di aver ripetuto un sacco di cose, così da riuscire a snellire ulteriormente il codice.

Dividere funzioni troppo lunghe in pezzi più piccoli è sempre un toccasana. Ovviamente il discorso vale anche per le classi, e in questo caso ci torna utile l'ereditarietà, ma anche altre tecniche come la composizione.

La regola base è: se la funzione è più lunga di 10 righe, prova a scomporla. Se la classe ha più di 10 metodi o 10 proprietà, prova a scomporla. Se il file cpp ha più di 200 righe, sei un cazzone perché non hai seguito le due regole precedenti.

Ereditarietà

Nella programmazione OO l'ereditarietà è una gran cosa. C++ è così avanti che ha pure l'ereditarietà multipla. Gran figata, ma bisogna saperla usare bene e con rispetto. Se usata bene, stando attendi a non incappare nel diamante, può produrre soluzioni eleganti che sarebbero più prolisse da sviluppare con altre tecniche come la composizione e la delegazione.

Diamo per scontato che non la sappiamo usare bene (ed è molto probabile che sia così) e continuiamo ad usare composizione e delegazione a scapito di essere prolissi. Oh, se veramente sei un guru del cipiùpiù, allora eredita pure a manetta, ma ricorda di non fare troppo lo sborone.

Macro is (not) evil

O meglio, il processore (non) è il male assoluto. Nella maggior parte dei casi c'è sempre un modo modo migliore per fare quello che ci permette il preprocessore. Ma a volte capita che una macro renda il codice più snello e semplice da leggere.

Perché non usare il preprocessore? Per vari motivi: non si possono debuggare le macro; l'espansione delle macro può portare a inaspettati effetti collaterali; non hanno namespace, così può succedere che vadano in conflitto con altre dichiarazioni; non hanno uno scope, esistono finché non viene chiamato #undef.

Solitamente le macro si possono sostituire con qualcosa di meglio. Perché usare #define ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE 42 quando uno static const fa lo stesso lavoro?

Quando abbiamo macro che simulano le espressioni, si possono probabilmente sostituire con:

  • funzioni normali
  • funzioni inline se vogliamo assicurarci che siano inline
  • funzioni template
  • funzioni constexpr (se usiamo C++11)

Ma non sono sempre cattive! Ci sono casi in cui veramente aiutano il codice ad essere più leggibile. Basta ricordarsi degli effetti collaterali e cercare di tenerli sotto controllo.

Molto utile è la stringification. Per esempio, mi capita spesso di assegnare alle variabili dei valori presi da file di configurazione. Una macro usata così rende il codice più snello, soprattutto se le variabili da leggere sono tante. Nell'esempio seguente vediamo come evitare di scrivere due volte il nome della variabile e due volte il tipo quando chiediamo il valore ad un property tree di boost.

#include <iostream>
#include <boost/lexical_cast.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>

int main() {
  boost::property_tree::ptree pt;
  boost::property_tree::json_parser::read_json("test.json", pt);                                                                                           
#define GET_VALUE(what, T) T what = pt.get<T>(#what);                                                                                                            
  GET_VALUE(one, int);
  GET_VALUE(two, float);
  GET_VALUE(three, unsigned);
  GET_VALUE(four, std::string);
#undef GET_VALUE
  print(one + two + three);                                                                                                             
  return 0;
}

Ogni GET_VALUE(what, T) viene sostituito dal preprocessore, diventando questo:

int one = pt.get<int>("one");
float one = pt.get<float>("one");
unsigned one = pt.get<unsigned>("one");
std::string one = pt.get<std::string>("one");

Con la macro si evita di ripetere due volte sia il tipo che il nome della variabile (che è lo stesso che si trova nel file di configurazione).

Altri casi in cui è bene usare le macro è per attivare/disattivare porzioni di codice in fase di compilazione. Con #ifdef e #if è possibile controllare che una condizione sia verificata nel momento della compilazione. Per esempio possiamo compilare una porzione di codice solo quando una dipendenza è di una certa versione:

#if OGRE_VERSION_MAJOR == 1 && OGRE_VERSION_MINOR == 6
void LogListener::messageLogged(const Ogre::String& message, LogMessageLevel lml,
  bool maskDebug, const Ogre::String& logName) {
#endif
#if OGRE_VERSION_MAJOR == 1 && OGRE_VERSION_MINOR == 7
void LogListener::messageLogged(const Ogre::String& message, LogMessageLevel lml,
  bool maskDebug, const Ogre::String& logName) {
#endif
#if OGRE_VERSION_MAJOR == 1 && OGRE_VERSION_MINOR == 8
void LogListener::messageLogged(const Ogre::String& message,
  LogMessageLevel lml, bool maskDebug, const Ogre::String& logName, bool&) {
#endif

Oppure possiamo attivare/disattivare il codice semplicemente aggiungendo comandando il compilatore. GCC permette di definire una macro con l'opzione -D. Nel prossimo esempio definisco la macro logDebug solo se la macro DEBUG è definita:

#ifdef DEBUG
#define logDebug(arguments)   log(7, arguments)
#else
#define logDebug(arguments)
#endif

In questo modo, quando compilo la versione di debug lo faccio con g++ -DDEBUG mentre quando compilo la versione di release non definisco quella macro. In questo modo la funzione log non solo non viene mai chiamata, ma nemmeno compilata! Utile? Per esempio per minimizzare la dimensione dell'eseguibile se lavoriamo in ambienti embedded oppure per migliorare il code obfuscation.

Questo articolo, iniziato a Luglio 2014, è diventato un po' troppo lungo, dopo varie integrazioni fatte negli ultimi mesi. Lo smetto qui, e continuo con nuovi post più corti, perché questa è la NOVECENTOQUARANTUNESIMA riga di markdown e comincio a sentire la stessa brutta senzazione di quando vedo un file cpp con così tante righe. 3 Ottobre 2014


TODO

  • namespace
  • il c++ non è c
  • stdlib
  • dove prendere esempio
  • librerie che aiutano a scrivere meglio
  • cicli e if
  • incapsulazione
  • headere e includes (namespace, ripetizione, macro...)
  • parametri di output
  • forward includes
  • inizializzare tutto
  • per ogni new un delete
  • return reference, pointer, copy
  • parametri out
  • l'ordine nell'implementazione
  • una dichiarazione per volta: int* a,b,c;
  • nomi standard (init, setup, clear, destroy...)
  • c++11
  • code smell http://en.wikipedia.org/wiki/Code_smell
  • anti pattern http://en.wikipedia.org/wiki/Anti-pattern

Fonti

La fonte principale è stata l'esperienza maturata negli anni, comprensiva di abomini letti e (sigh!) scritti. Poi, facendo ricerche per rendere questo articolo il più completo possibile, mi sono imbattuto in ottimi testi, quali:

Ti è piaciuto l'articolo? Condividilo! Commentalo!

comments powered by Disqus