Salve a tutti.
Sulla Rete sono presenti innumerevoli style guide e coding convention per i più svariati linguaggi di programmazione: si tratta di documenti che contengono un insieme di regole, normalmente ben ponderate, che permettono di ottenere un codice elegante, coerente/consistente e intrinsecamente (più) affidabile. In altre parole una “guida di stile” dovrebbe incarnare o favorire un insieme di best practices.
In questo post ve ne presenterò una molto interessante, ossia la “Google C++ Style Guide“. Intendo analizzarne le varie sezioni con spirito costruttivo ma anche critico. Dal momento che tale guida è piuttosto lunga, anche i miei commenti lo saranno: ho quindi optato per suddividerli in due o più parti.
Premessa
Prima di addentrarci nei contenuti di questo documento, è opportuno notare come non esista nè possa esistere una “guida di stile” suprema ma, al massimo, possiamo parlare di un qualcosa di ottimale in un determinato contesto.
Inoltre questo genere di guida riflette spesso il punto di vista e l’esperienza sulla “materia” di un individuo o di un gruppo di individui (es: sviluppatori di un’azienda), quindi è naturale che il resto del mondo possa trovarsi in aperto contrasto con una o più delle “regole” o consigli proposti.
Infine, nel caso specifico, non ho intenzione di soffermarmi su ogni singola regola ma di analizzare piuttosto quelle che ritengo particolarmente interessanti e significative.
Sezione “Header Files”
In questa sezione vengono presentate utili regole-suggerimenti per gestire al meglio gli header file.
Regola: “In general, every .cc file should have an associated .h file.”
In realtà questa regola ne cela due: i file sorgenti hanno l’estensione .cc e ognuno di essi, ragionevolmente, ha il suo file header .h corrispondente.
Sull’estensione dei file sorgente non esiste una regola precisa o “standard”, quindi si può far come meglio si crede: c’è chi usa .cc, .C, .cxx, .cp, .cpp, … Personalmente preferisco di gran lunga .cpp anche perchè .cc non mi riporta intuitivamente al C++. De gustibus…
Quanto al fatto che a ogni file sorgente corrisponda un file header, direi che se il primo è davvero piccolo, il secondo diventa opzionale. In tutti gli altri casi la regola è valida.
Regola: “The #define Guard. All header files should have #define guards to prevent multiple inclusion. The format of the symbol name should be <PROJECT>_<PATH>_<FILE>_H_.”
Sempre in questa sezione di parla del “classico” utilizzo delle #define “a guardia” delle inclusioni di codice (per evitare che si includano più volte gli stessi file).
A dire il vero non mi piace molto il prolisso formato con quel _<PATH>_ messo così: in caso di cambiamenti/refactoring potrebbe risultare anche noioso da sistemare.
In altre parole, non mi piace indicare percorsi perchè, a differenza dei package Java (che grossomodo sono rimappati/gestiti gerarchicamente come directory, incluse o meno in file compressi .jar), bisogna anche manutenerli. Per forza poi si favoriscono i namespace anonimi!
Regola: “Inline Functions. Define functions inline only when they are small, say, 10 lines or less.”
Mi trovo perfettamente d’accordo con questa euristica sulle funzioni “inline“: 10 righe di codice mi pare sia un limite accettabile per marcare inline una funzione, anche perchè poi sarà il compilatore a decidere se è il caso di procedere o meno. Per il compilatore, “inline” è solo un “hint“, un suggerimento e nulla più.
Ricordo lo standard C++ prevede l’inlining automatico per tutte le funzioni dichiarate all’interno del corpo di una classe o struttura. Dal manuale del GCC:
“As required by ISO C++, GCC considers member functions defined within the body of a class to be marked inline even if they are not explicitly declared with the inline keyword. You can override this with -fno-default-inline; see Options Controlling C++ Dialect.”
Regola: “Function Parameter Ordering. When defining a function, parameter order is: inputs, then outputs.”
Direi che questa regola per l’ordinamento dei parametri nelle funzioni è onesta e integrabile:
Ho detto “integrabile” perchè vale la pena aggiungere che:
- una funzione non deve avere più di 7 parametri (come direbbe Steve McConnell);
- la dichiarazione dei parametri non dovrebbe solo seguire la distinzione input-poi-output ma essere organizzati in base alla priorità: i parametri essenziali vanno messi quanto più possibili vicino al nome della funzione ossia più a sinistra possibile e più a destra quelli opzionali/secondari (se è il caso, magari con valori di default);
- i parametri non vanno mai messi in ordine alfabetico fra loro.
Regola: “Names and Order of Includes. Use standard order for readability and to avoid hidden dependencies: C library, C++ library, other libraries’ .h, your project’s .h.”
Questa regola mi piace tuttavia penso che manchi (in maniera vistosa per una guida del genere) una qualche esplicita raccomandazione che imponga di usare i file “C++-style” per gli header della libreria C.
In altri termini:
// header C secondo lo stile (e gli eventuali controlli aggiuntivi del) C++ #include <cstdlib> #include <cstdio> #include <cmath> // header C nella "forma canonica" #include <stdlib.h> #include <stdio.h> #include <math.h>
Sezione “Scoping”
In questa sezione si parla di regole di scoping e devo dire che ci sono alcuni punti con i quali non mi ritrovo.
Regola: “Namespaces. Unnamed namespaces in .cc files are encouraged. With named namespaces, choose the name based on the project, and possibly its path. Do not use a using-directive.”
A dire il vero non mi piace molto l’idea di “namespace senza nome” anche se permette di diminuire la pollution (e i name-clash) dei namespace e, soprattutto, a “nascondere” varibili altrimenti dichiarate globali. Preferisco comunque mettere un nome significativo ai namespace, che mi permetta di raggruppare classi, strutture, … con una “etichetta”, il namespace appunto.
Posso anche capire l’intenzione della sotto-regola:
Do not use a using-directive.
ma se la si prende in senso letterale si rischia di dover scrivere sempre il nome “esteso” (che francamente rende il codice più difficile da leggere) o ricorrere a molte macro #define.
Da quello che ho letto in giro è bene utilizzare clausole “using” ristrette per “importare” solo quello che serve, alla stregua di quanto si consiglia in altri linguaggi come Java e C#.
//ESEMPIO: usare le stringhe dello standard C++ // scorciatoia con clausola using namespace // (perchè importare tutto il namespace per usare solo le stringhe?) using namespace std; string test; // scorciatoia con clausola using "ristretta": // si "usa" solo quello che serve using std::string; string test; // scorciatoia tramite macro del preprocessore #define string std::string string test; // utilizzo diretto, senza scorciatoie std::string test;
Usare i percorsi (path) – o qualcosa che li ricorda – per i namespace fa molto Java (cfr. nomi dei package).
Se il punto è gerarchizzare i namespace per evitare ulteriormente le collisioni, preferisco mille volte nidificarli piuttosto che definirli/accedervi con un nome di percorso: in fase di un eventuale refactoring probabilmente questi percorsi andrebbero modificati a loro volta.
In altre parole, a meno che io non abbia frainteso la regola, sembra quasi che si debbano inserire percorsi hard-coded come nomi dei namespace.
Regola: “Local Variables. Place a function’s variables in the narrowest scope possible, and initialize variables in the declaration.”
Sull’inizializzare sempre la varibili quando le si dichiara nulla da eccepire. A differenza di Java, il C++ non inizializza nulla automaticamente quindi è bene fare attenzione.
Anche sul dichiarare le variabili nello scope più ristretto possibile non ho particolari obiezioni.
void test(void)
{
for(int i=0; i<10; i++) // i "vale" solo in questo ciclo for (riusabile fuori)
{
// fai qualcosa
}
return; // implicito/superfluo ma più elegante/coerente (e pedante)
}
Aggiungo inoltre che detesto la dichiarazione multipla di variabili dello stesso tipo su una sola riga perchè può confondere le idee:
// dichiarazione di più variabili dello stesso tipo // su una sola riga (solo a è un puntatore) long* a, b, c; // dichiarazione di variabili stesso tipo, una per riga // (l'asterisco vicino al nome della variabile 'a' lo rende // maggiormente evidente) long *a; long b; long c;
Sezione: “Classes”
In questa sezione si parla di regole di classi e struct.
Regola: “Copy Constructors. Use copy constructors only when your code needs to copy a class; most do not need to be copied and so should use DISALLOW_COPY_AND_ASSIGN.”
Molto interessante il discorso sui costruttori di copia definendo e usando quella macro DISALLOW_COPY_AND_ASSIGN che esplicitamente marca le classi che non devono averlo.
La macro poi verrà espansa proprio in una definizione “vuota” di un costruttore di copia e dell’operatore di assegnamento, operator=, impedendo quindi di (ri)definirli.
Regola: “Structs vs. Classes. Use a struct only for passive objects that carry data; everything else is a class.”
In linea di massima non ho nulla da obiettare su questa regola per un utilizzo diversificato e preciso di strutture e classi.
Per quanto la regola sia simile all’indicazione del padre del C++, Stroustrup, contenuta nel suo libro “C++ Programming Language, preferisco quest’ultima “versione”:“:
“Which style you use depends on circumstances and taste. I usually prefer to use struct for classes that have all data public. I think of such classes as “not quite proper types, just data structures.”
Personalmente, essendo struct e class intercambiabili e mischiabili, fermo restando il loro diverso comportamento di default (per struct il default è public, per class invece private), tendo a preferire class per i tipi che hanno metodi.
Ammetto un’importante eccezione a questa “mia” regola, ossia quando ho limiti di spazio, ho solo metodi pubblici e voglio mantenere ragionevolmente compatto il codice (es: quando posto su questo blog).
Regola: “Inheritance. Composition is often more appropriate than inheritance. When using inheritance, make it public.”
Spesso capita che una classe A necessiti di qualcosa presente in un’altra B. Normalmente, a meno di limitazioni, le opzioni sono due:
- si usa l’ereditarietà: A estende B e usa quel che serve;
- si usa la composizione: A include un oggetto di B e lo usa (in questo caso si parla di object aggregation).
La composizione spesso è preferibile:
- a livello concettuale, se A e B hanno pochissimo o addirittura nulla in comune, perchè A dovrebbe estendere B?
- a livello pratico: sfavorisce l’abuso dell’ereditarietà multipla (vedi regola successiva)
Regola: “Multiple Inheritance. Only very rarely is multiple implementation inheritance actually useful. We allow multiple inheritance only when at most one of the base classes has an implementation; all other base classes must be pure interface classes tagged with the Interface suffix.”
Al di là del problema del diamante, quando si ha a che fare con una miriade di oggetti ognuno dei quali ne estende altri e così via, cercare di capire la struttura gerarchica diventa veramente un incubo.
Si capisce quindi perchè l’ereditarietà multipla, feature tipica del C++, non è stata ripresa da molti dei suoi linguaggi “successori” inclusi Java e C#. La regola di Google impone infatti quanto avviene in questi due ultimi linguaggi: ereditarietà singola ma possibilità di implementare un numero illimitato di interfacce.
Regola: “Interfaces. Classes that satisfy certain conditions are allowed, but not required, to end with an Interface suffix.”
Il C++ non ha una chiara keyword come Java per marcare esplicitamente un’interfaccia così esistono vari modi per indicarlo. Personalmente preferisco usare il prefisso I invece del lungo e prolisso postfisso Interface (es: IContract invece di ContractInterface).
Analogamente per quanto riguarda le classi astratte (quindi non interfacce) da un po’ di tempo ho optato per imitare Moris (collega che saluto
) usando il prefisso A (es: ASomething).
Regola: “Operator Overloading. Do not overload operators except in rare, special circumstances.”
L’overloading degli operatori è una feature del C++ che mi è sempre piaciuta e che talvolta si rivela molto interessante. Abusarne è ovviamente sempre controproducente, specie quando se ne ridefinisce uno in maniera non intuitiva.
In alcuni casi è però indispensabile: ad esempio è necessario ridefinire operator() per ottenere un functor (sull’argomento mi sono divertito abbastanza).
Regola: “Access Control. Make all data members private, and provide access to them through accessor functions as needed. Typically a variable would be called foo_ and the accessor function foo(). You may also want a mutator function set_foo().”
Nulla da eccepire sul contenuto. Personalmente preferisco marcate esplicitamente gli accessori get/set in modo da essere facilmente ricercabili e trovabili. Ad esempio, se uso un IDE con autocompletamento, mi basta digitare get per trovare tutti gli accessori disponibili di una specifica classe. Medesimo discorso con set.
Il fatto di usare foo() e set_foo() come suggerito da Google non mi piace anche per un discorso di asimmetricità (per i nomi dell’accessore e del mutatore).
Inoltre, riclicare più volte e con significati/contesti diversi lo stesso nome nello stesso codice (in questo caso per una data variabile e per il relativo accessore) è una prassi rischiosa, sconsigliabile e quindi da evitare.
// 3 nomi distinti, simmetricità nei nomi
// fra accessore e mutatore (get/set)
class Test
{
private:
int foo_;
public:
int getFoo()
{
return foo_;
}
int setFoo(int newValue)
{
foo_ = newValue;
}
}
Regola: “Declaration Order. Use the specified order of declarations within a class: public: before private:, methods before data members (variables), etc.”
Qui va a gusti. Io preferisco l’opposto, ma solo per un discorso estetico e di abitudine: io ordino per private, protected e infine public.
Dovendo cercare una motivazione per giustificarlo/mi direi che è una delle mie fisse: guardando dal punto di vista di una classe come elemento a sè stante, ciò che è marcato come privato è qualcosa di esclusivo e normalmente fondamentale. Quindi va in alto, in prima posizione.
Regola: “Write Short Functions. Prefer small and focused functions.”
Nulla da eccepire. Altre guide parlano di un massimo di 100 righe per funzione, ma il limite di 40 mi pare decisamente accettabile.
Dal momento che adotto lo stile Allmann conto le righe “con contenuto”, escludendo quindi le righe con le sole graffe.
Nota: sono molto intransigente su questo genere di discorso. Prima o poi ne parlerò…
Quanto al resto, concordo: a ogni funzione un solo compito, ben definito.
Sezione “Google-Specific Magic”
In questa sezione si parla di regole specifiche di Google come realtà aziendale.
Regola: Smart Pointers. If you actually need pointer semantics, scoped_ptr is great. You should only use std::tr1::shared_ptr under very specific conditions, such as when objects need to be held by STL containers. You should never use auto_ptr.
La regola è corretta. Prossimamente il nuovo standard C++ (C++0x) includerà nuovi tipi di smart_pointer derivati da quelli delle Boost C++ Libraries.
Si noti che a quanto pare uno degli smart_pointer attuali, auto_ptr, verrà dichiarato deprecato in C++0x.
Regola: cpplint. Use cpplint.py to detect style errors.
Interessante script per controllare il rispetto delle regole-Google nei sorgenti C++.
Conclusioni!
Per ora vi saluto… alla prossima puntata!
Ciau! ^^
Ciao JP,
complimenti! altro ottimo post, interessante e soprattutto utile!
Mi permetto di commentare alcuni punti (non voglio assolutamente contraddire nessuno o insegnare nulla a nessuno anche perchè non posso permettermelo)…
1) keyword inline
Ho letto su parecchi libri che porre questa parola chiave davanti ad una funzione (come hai detto tu quelle nell’header sono implicitamente inline) sia un suggerimento al compilatore in quanto è lui stesso ad accettare o meno la dicitura. Infatti con funzioni troppo “lunghe” o che richiamano altre funzioni non verrà presa in cosiderazione. Sempre dalle letture, si parlava di massimo 3 o 4 righe; le funzioni con più di 4 righe non vengono prese in considerazione dal compilatore.
2) Costruttore di copia
Sempre nelle mie letture (il libro dei 2 Deitel) si consiglia di aggiungre (o meglio sovvrascrivere quello di default, ad ogni classe il costruttore di copia non viene aggiunto?) quando la classe ha almeno un membro puntatore. Sempre in questo caso, va fatto anche l’override dell’operatore di uguaglianza (operator==).
3) Ordine della dichiarazione di classe
Concettualmente è più corretto (sempre per le mie [scarse] conoscenze ) dichiarare prima i membri e metodi public, poi protected e infine private. Questo perchè chi vede l’header (pensa ad un ipotetico utilizzatore di una libreria) ha bisogno SOLO di vedere i metodi pubblici cioè in altre parole non ha bisogno e non DEVE (dovrebbe) vedere metodi e variabili privati che – in qualche modo – danno informazioni circa l’implementazione della classe stessa. Proprio per questo, per eliminare qualsiasi traccia dell’implementazione di una classe, vengono utilizzate le classi proxy.
Spero di non aver scritto troppe fesserie e in quel caso chiedo venia.
Saluti, Martino
Ciao!
Direi che non hai scritto fesserie, anzi!
1) 10 righe è il limite superiore e, come al solito, dipende dal numero di righe “effettive” che uno ha. Nel mio caso, prediligendo lo stile Allman, immancabilmente svariate righe sono di fatto vuote (solo le parentesi graffe).
Inoltre ogni compilatore è libero gestire l’hint (l’ho aggiunto esplicitamente) “inline” come meglio crede.
2) Sì, se non lo fai tu lo fa il compilatore al tuo posto e, parafrasando i siori di Google proprio in quella regola,
“Most classes do not need to be copyable, and should not have a copy constructor or an assignment operator. Unfortunately, the compiler generates these for you, and makes them public, if you do not declare them yourself.”
Medesimo discorso per i costruttori di default:
“You must define a default constructor if your class defines member variables and has no other constructors. Otherwise the compiler will do it for you, badly.”
Nota: anche io ho il primo dei due libri del C++ dei Deitel (quello che parla anche di UML no?) ma a dire la verità non mi è piaciuto granchè…
3) Quello che dici ha molto senso. Come ho detto l’ordine inverso è una mia abitudine (motivata mi pare) e nulla più
Grazie per il commento! ^^
Hello JP,
concordo con Matino per l’ordine delle dichiarazione nelle classi: io la vedo sempre dal punto dell’utilizzatore (e poi vengo dal Modula2) e quindi prima di tutto quello che serve all’utilizzatore
Invece sempre relativamente ai punti di vista personali, io odio usare extension per i nomi (tipo I o Interface, A o Abstract)… non lo ritengo molto utile e in più è qualcosa in più da mantenere (a livello di codice e di documentazione): non è bello che ad una seconda release di un libreria (e mi è successo di vederlo) trovarsi una classe astratta che si chiama IPippo
Comunque strano che google non abbia anche dichiarato esplicitamente se preferisce la definizione dei blocchi alla Kernighan & Richie o in qualche altro modo
o se per i nomi dei metodi bisogna usare la camel notation o la undersore notation… come sempre “le regole sono belle… perché si possono infrangere”
cheers
I/Interface/A/Abstract/… => consuetudini e nulla più. Sono una versione ristrettissima e class-oriented della notazione ungherese (che per altro non mi piace). LOL.
Quanto al resto: caaaalma! Aspetta la seconda parte.
Ciao,
ieri sera stavo pensando all’affermazione di Google “Copy Constructors. Use copy constructors only when your code needs to copy a class; most do not need to be copied“, che alle mie orecchie suona strana… visto che nelle realtà che ho visto molto spesso c’è la necessità di poter copiare le classi (o le strutture)… e poi ho capito il senso con cui lo dicevano che può essere sintetizzato con due sotto regole:
- se devi copiare una classe o una struttra copia via reference (nel caso non hai fatto il new della classe/struttura) …
- … oppure usa uno smart pointer
cheers
Ri-ciau.
Non credo si tratti di un’idea malsana quella di bloccare gli oggetti impedendone la copia/cloning. Premesso che non ho mai “bloccato” un oggetto, però se impedisco al compilatore di fare al posto mio, ottengo un minuscolo (infimo) risparmio di spazio.
Il prossimo standard renderà comunque il trucco con la #define di Google inutile e in un modo molto elegante (cfr. Wikipedia)
Ciau!
Ciao,
ho dato un occhiata al link su wiki che hai postato… e come sempre non sono d’accordo su questi cambiamenti nella sintassi che non portano nulla di nuovo al linguaggio… non vedo perché devo cambiare da (standard attuale)
struct NonCopyable
{
private:
NonCopyable & operator=(const NonCopyable&) ;
NonCopyable(const NonCopyable&);
}
a (futuro standard):
struct NonCopyable
{
NonCopyable & operator=(const NonCopyable&) = delete;
NonCopyable(const NonCopyable&) = delete;
};
dove è il vantaggio?
IMHO queste “novità” servono solo a cercare di *far sembrare* il linguaggio C++ più user-frendly… quando in realtà i veri problemi sono altri… e non dipendono dal linguaggio.
cheers
Tutto quello che aumenta la leggibilità e con poco sforzo, IMHO, merita di essere considerato.
Delle due varianti la seconda è molto più intuitiva (nulla ti vieta di continuare a usare la prima forma). Si parla di cosmetics, ovviamente…
PS: a questo punto mi piacerebbe anche una…
virtual foo() {} = pure; // ok, lo so, basta una #define pure 0nello standard o ancora meglio…
pure virtual foo() {}In altre parole = 0 messo così mi fa veramente pena…
Bel post, ho anche scoperto dell’esistenza del SyntaxHighlighter che mi piace molto !
Faccio presente che, in generale, le guide di stile non pretendono di dettare nuovi standard per un linguaggio, ma servono per evitare che in un progetto ci siano tanti stili di programmazione quanto sono i programmatori che vi lavorano. Un codice scritto troppo ‘freestyle’ risulta, oltre che brutto, anche difficile da leggere e costoso da mantenere.
In questo senso le linee guida possono essere assolutamente arbitrarie: potete anche stabilire che tutte le variabili che cominciano per ‘A’ siano scritte con lettere maiuscole …
Poi, è ovvio che se si pubblicano queste linee guida e chi le legge le trova utili anche per i propri progetti, allora con il tempo può nascere un coding standard..
P.S. Nei miei progetti le style guides cerco di tenerle sempre ridotte al minimo, una paginetta Wiki al massimo: non ha senso che i programmatori debbano impazzire più di tanto: meglio che scrivano del codice funzionante …
Vero: le linee guide sono il minimo comune denominatore per varie persone che lavorano su un progetto.
Detto questo, escluso il caso di codice “legacy” scritto da altre mani, per i progetti nuovi è bene definire anzitempo regole chiare, concordate, ponderate e possibilmente durevoli nel tempo.
La prima regola di JP sui coding style è: “più il codice è “pedante”, più è intelligibile e quindi servono meno regole“. Prima o poi spiegherò bene cosa intendo per “pedante”.
Ciao & grazie! ^^