C++ Style
C++ Style
Guida di Stile
Marco Trivellato
[email protected]
http://www.x-gdk.org/
1. Introduzione
1.1. Motivazioni
2. Regole generali
2.1. Tipi di file
2.2. Inclusioni
2.3. File Header
2.4. Dichiarazioni
2.5. Puntatori e References
3. Documentazione
3.1. Commenti
3.2. Generazione automatica
4. Nomi
4.1. Criteri di assegnazione
4.2. Variabili
4.3. Puntatori
4.4. Costanti
4.5. Funzioni
4.6. Classi
4.7. Metodi
4.8. Parametri
5. Formattazione
5.1. Indentazione
5.2. parentesi graffe
5.3. uso di spazi e tab
5.4. linee lunghe
5.5. if-then-else
5.6. for
5.7. while
5.8. switch
6. Classi
6.1. Costruttori
6.2. Distruttori
6.3. Accesso ai dati
6.4. Specificatore const
6.5. Membri statici
6.6. Overloading degli operatori
6.7. Oggetti temporanei
7. Portabilità
7.1. Uso del typedef
7.2. #define condizionali
8. Ottimizzazione del codice
8.1. Assembler
8.2. Inlining
8.3. Template
9. Varie
9.1. Costanti
9.2. direttiva #define
9.3. new e delete
9.4. Passaggio di parametri
9.5. Valori di ritorno
9.6. Namespace
9.7. Cast
9.8. RTTI
9.9. Puntatori a funzione
9.10. continue, goto, break, return
9.11. Altri consigli
10. Conclusioni
11. Links Utili
12. Bibliografia
1 Introduzione
L’obiettivo è quello di fornire una serie di regole che aiutino a scrivere codice in modo da
migliorarne alcune caratteristiche fondamentali:
• Leggibilità
• Manutenibilità
• Portabilità
Le regole e i consigli che verranno presentati sono derivanti da un’esperienza personale
di programmazione in C++ nel campo dei videogiochi in cui bisogna imparare a risolvere
problemi di ogni genere tenendo sempre in constante considerazione le prestazioni.
Nonostante ciò credo che questo documento posssa essere utile anche a chi si occupa di
altre problematiche.
1.1 Motivazioni
2 Regole Generali
I nomi dei file dovrebbero essere sempre coerenti con il loro contenuto. Quindi è
importante che il nome e l’estensione dei file siano significativi.
Per quanto riguarda le estensioni bisogna dire che non esiste uno standard per i file c++,
infatti, alcuni compilatori come VisualC++ ed Borland C++ Builder usano di default i .cpp,
CodeWarrior usa i .cp, mentre chi usa gcc molto spesso è abituato ad usare i .cc.
Inoltre, la distinzione codice c e c++ deve essere fatta anche per gli header quindi, anche
in questo caso, bisogna utilizzare l’estensione appropriata.
A questo proposito è utili definirle per evitare che persone che lavorano allo stesso
progetto utilizzino estensioni diverse tra loro.
Alcuni compilatori come il VisualC++ ed il C++ Builder non sono predisposti a compilare
file .cc ma è possibile abilitarli a riconoscere questa estensione con delle apposite opzioni.
2.2 Inclusioni
L’uso della direttiva di preprocessore #include deve essere fatto con un certo criterio. Nel
senso che bisogna includere il minor numero di file in modo da diminuire le dipendenze tra
i file.
Innanzitutto, il .hh di una classe deve includere solo i file delle classi da cui deriva, il .cc
invece include tutti i file che riguardano i tipi di cui ha bisogno. Nel caso in cui una classe
utilizzi dei tipi di dato che non sono definiti, ovviamente il compilatore genere un errore del
tipo ‘undefined symbol’ o ‘declaration error’.
In questi casi, la tentazione di includere tutti i file di cui si ha bisogno direttamente nel .h è
sicuramente molto forte, soprattutto per chi è alle prime armi con il C++ ma si deve
cercare di sostituire queste inclusioni con delle dichiarazioni anticipate (pre-declarations).
Vediamo un esempio:
#include “bitmap.hh”
class Button{
public:
Bitmap* GetBitmap( );
void SetBitmap( Bitmap* bitmap);
};
class Button{
public:
Bitmap* GetBitmap( );
void SetBitmap( Bitmap* bitmap);
};
Per evitare problemi di doppia inclusione, ogni header dovrebbe avere questa struttura:
#ifndef MYHEADER_H
#define MYHEADER_H
#pragma once
// dichiarazioni
#endif // MYHEADER_H
La direttiva once serve ad indicare che il file deve essere indicato una sola volta. Quindi si
puó dire che ha la stessa funzione dell’#ifndef. La differenza sta nel fatto che un metodo
definisce un simbolo di preprocessing e l’altro è un settaggio del compilatore. Usarli
insieme serve ad avere la certezza che il file non venga incluso piú di una volta.
2.4 Dichiarazioni
Puntatori e reference devono essere dichiarati rispettivamente con ‘*’ e ‘&’ a fianco del
tipo, oppure accanto al nome. E’ preferibile la prima soluzione.
Button* pCancel;
oppure
Button *pCancel;
Button* pCancel;
Button* pOk;
questa soluzione risolve qualsiasi ambiguità ma ad alcuni potrebbe non piacere, in tal
caso è possibile definire un tipo di dato da usare come puntatore:
class Button{
...
};
3 Documentazione
3.1 Commenti
Anche per quanto riguarda i commenti non esistono regole precise ma seguire delle
convenzioni aumenta la leggibilità del codice ed aiuta il lettore a ricavarne informazioni
che, altrimenti, potrebbe avere solo nel caso in cui avesse la possibilità di chiederle
direttamente all’autore.
I commenti sono essenziali e possono essere considerati come parte integrante del
codice.
All’inizio di ogni file ci dovrebbe sempre essere un’intestazione in cui ci sono informazioni
riguardanti:
• contenuto del file
• autore o autori
• versione
• data di creazione e di ultima modifica
• note sull’utilizzo
eventualmente è possibile inserire una “storia” del file in cui vengono indicate le modifiche
che sono state effettuate.
I commenti al codice devono essere sintetici e significativi. Non devono comparire a fianco
di ogni istruzione, come accade per listati in assembler ma dovrebbero spiegare in modo
essenziale ciò che l’autore vuole ottenere da un certo numero di istruzioni senza duplicare
informazioni, infatti, i commenti devono necessariamente spiegare qualcosa che non si
può capire direttamente dal codice.
/*!
* \class Base
*
* \brief base class containing name, object and class information
*
* \author Marco Trivellato
*
* \version 1.0
*
* \date 05.dec.1999 - 11.jan.2001
*
* \internal
*
* Bugs?: Please send this file with the problem
* description to [email protected]
*/
4 Nomi
m membri di classi
s membri statici
p Puntatori
h Handle
g variabili globali
Ogni parola che segue la prima, deve iniziare con la lettera maiuscola.
class IndexBuffer{
public:
IndexBuffer( int indexCount)
: mIndexCount( indexCount)
{
...
}
...
private:
int mIndexCount;
...
};
Assegnare un nome ad una variabile, ad un metodo o a una classe è una delle prime
operazione che un programmatore deve fare quando scrive le sue prime linee di codice.
Spesso accade che questa operazione venga fatta in modo superficiale, ottenendo così
dei nomi poco significativi oppure, nel peggiore dei casi, non adatti.
Sono da evitare dichiarazioni del tipo:
EditField edit1, edit2, edit3;
questi nomi non danno alcuna informazione aggiunta al loro tipo e rischiano di confondere
anche l’autore stesso.
I nomi devono essere significativi e bisogna convincersi che il tempo speso per trovare un
nome deve essere considerato come tempo risparmiato durante la manutenzione del
codice. Il nome perfetto non esiste ma esistono solo nomi che calzano meglio e che quindi
sono più significativi. Non stupitevi di cambiare un nome anche dopo alcuni mesi, se vi
accorgete che un determinato nome non vi soddisfa e ne avete trovato uno che si adatta
meglio, allora dovete cambiarlo.
4.2 Variabili
Esistono diverse scuole di pensiero sui nomi da dare ad un membro di una classe o ad
una semplice variabile dichiarata nello scope di un metodo. Una convenzione molto
diffusa nell’ambiente Windows che è assolutamente da evitare è l’Hungarian-notation, in
base alla quale ogni nome di variabile deve essere preceduto da una serie di caratteri che
ne identificano il tipo. In molti caso questi nomi diventano molto lunghi e il tempo che
bisogna spendere per dichiarare una variabile non è giustificato dal vantaggio di sapere in
qualsiasi momento il suo tipo, soprattutto adesso che queste informazioni sono fornite
direttamente dall’ambiente di sviluppo.
4.3 Puntatori
L’unico postfisso riguarda i tipi dei puntatori, i quali devono essere seguiti da Ptr:
class Bitmap{
...
};
4.4 Costanti
4.5 Funzioni
Per quanto riguarda i nomi delle funzioni vi sono due convenzioni particolarmente diffuse
che differenziano solo per la lettera iniziale. Una che ”vuole” la prima lettera minuscola,
come ad esempio la convenzione Java, ed l’altra in cui è maiuscola come nel caso delle
API di Windows.
Noi useremo la seconda convenzione, quindi le procedure avranno nomi di questo tipo:
Image* CreateImage( int width, int height, int depth);
4.6 Classi
I nomi delle classi iniziano tutti con una lettera maiuscola. Alternativamente possono
essere preceduti da un prefisso di comune a tutto il progetto, ad esempio Sprite potrebbe
diventare GfxSprite. In genere il prefisso non dovrebbe superare le 3 lettere.
class GfxBitmap{
...
};
class GfxSprite{
...
};
namespace Gfx{
class Bitmap{
...
};
class Sprite{
...
};
}
Le variabili membro devono essere preceduti dalla lettera m, senza l’underscore a
seguire. Ad esempio:
class Bitmap{
private:
int mWidth;
int mHeight;
};
4.7 Metodi
4.8 Parametri
Il C++ permette di omettere il nome dei parametri in ingresso dei metodi di una classe,
definendone solo il tipo. Ecco un esempio:
class List{
public:
void Insert(Node*);
};
class List{
public:
void Insert(Node* pItem);
};
5 Formattazione
5.1 Indentazione
Per migliorare la leggibilità del codice, è necessario scrivere in modo che il codice possa
essere letto senza troppi problemi nell’individuare l’inizio o la fine di un costrutto. Ogni riga
non deve avere più di una istruzione.
Per quanto riguarda le parentesi graffe ci sono principalmente due correnti di pensiero.
Una molto diffusa tra i programmatori per sistemi Linux:
if (cond) {
istruzione1
istruzione2
}
una variante è quella in cui la parentesi aperta si trova allineata con la parentesi chiusa:
if (cond)
{
istruzione1
istruzione2
}
if (cond)
{
istruzione1
istruzione2
}
istruzione1
if (cond)
{
istruzione2
istruzione3
}
ovviamente questo è un esempio molto semplice ma puó capitare di avere dei costrutti
annidati in cui diventa difficile capire dove sia l’inizio e la fine dei vari scope.
Per indentare il codice non bisogna usare gli spazi, bensì il tab preferibilmente da 4
caratteri.
Gli spazi, invece, devono essere utilizzati altrove per migliorare la leggibilità.
• Passaggio di parametri
• assegnamenti
diventa:
5.5 Condizioni
pButton->Draw();
}
pButton->Draw();
}
come si può notare, questo accorgimento comporta una piccola modifica al codice ma che
sicuramente rende più immediato il suo significato.
E volendo eliminare ogni pericolo dovuto ad assegnamenti non voluti è possibile scrivere:
if ( nil != pButton) {
pButton->Draw();
}
Dove necessario, i commenti devono essere scritte su più di una riga. Ad esempio:
istruzione1;
istruzione2;
}
diventa:
if ( cond1 &&
cond2 &&
cond3) {
istruzione1;
istruzione2;
}
5.5 if-then-else
L’if-then-else è uno dei costrutti più utilizzati, perciò anche in questo caso bisogna
uniformare il modo in cui viene scritto.
Ecco come dovrebbe essere scritto in queste diverse situazioni:
if (condizione)
istruzione
oppure
if (condizione) {
…
}
if (condizione) {
…
}
else {
…
}
In situazioni in cui compiono più if annidati è preferibile l’uso delle parentesi graffe anche
se vi è una sola istruzione da eseguire. Ad esempio:
if (cond)
if (cond)
istr1;
else
istr2;
il ramo else è riferito al primo o al secondo if? Per il compilatore si riferisce al secondo e
probabilmente è proprio quello che vuole ottenere chi lo ha scritto ma è molto facile
incorrere in errori che non sempre si individuano facilmente.
In questo modo risulta molto più chiaro e non lascia spazio ad ambiguità.
if (condizione) istruzione
Risparmiare una riga non serve assolutamente a nulla e rende il codice di difficile lettura,
soprattutto quando questo tipo di indentazione viene ripetuto più di una volta.
Un altro motivo per non scrivere l’if su una sola riga è la fase di debug, infatti, risulta molto
scomodo usare dei breakpoint scrivendo in questo modo.
5.5 for
potrebbe diventare:
...
5.6 while
oppure
while (value++ < N)
{
}
sono equivalenti ma nel caso in cui si voglia usare il primo tipo è consigliabile inserire un
commento.
5.7 switch
Quando possibile, è sempre meglio usare lo switch piuttosto che una serie di if che
testano lo stesso dato.
switch (value) {
case kRed :
...
break;
case kGreen:
...
break;
case kBlue:
...
break;
default :
// Unknown value
break;
}
switch (value) {
case kRed :
...
break;
case kGreen:
...
case kBlue:
...
break;
default :
// Unknown value
break;
}
Come si può notare, nel case kGreen manca il break. Ciò è perfettamente lecito ma
sarebbe più chiaro se venire inserito un commento per esplicitare che l’esecuzione
continua dalla prima istruzione del case kBlue:
switch (value) {
case kRed :
...
break;
case kGreen:
...
// continua
case kBlue:
...
break;
default :
// Unknown value
break;
}
L’ultimo consiglio per lo switch è quello di inserire sempre il caso default. In questo modo
si rende il codice più esplicito.
6 Classi
Ogni classe del progetto è associata a due file: il .hh che contiene la dichiarazione ed il .cc
l’implementazione.
A questa regola fanno eccezione le classi template, le quali non hanno un .cc ma solo il
.hh.
Infatti, il corpo dei metodi si trova dopo la dichiarazione della classe stessa:
#ifndef VECTOR_H
#define VECTOR_H
template<class T>
class Vector{
public:
Vector( unsigned int size);
T& operator[] ( unsigned int index);
...
protected:
T* mpItems;
unsigned int mSize;
...
};
template<class T>
T&
Vector<T>::Vector( unsigned int size)
: mSize( size)
{
...
}
template<class T>
T&
Vector<T>::operator[]( unsigned int index)
{
return mItems[index];
}
#endif // VECTOR_H
I membri di una classe dovrebbero essere sempre dichiarati con questa sequenza:
class MyClass{
public:
...
protected:
...
private:
...
#include “Bitmap.hh”
namespace gfx{
#endif // MYBITMAP_H
6.1 Costruttori
Ogni classe dovrebbe fornire un costruttore di default. Se ciò non avviene è il compilatore
che si preoccupa di fornirlo ma in questo caso il codice potrebbe risultare poco chiaro e si
dovrebbe modificare la dichiarazione della classe nel momento in cui si decidesse di
implementarlo.
Un costruttore dovrebbe solo inizializzare i membri dell’oggetto senza eseguire altre
operazioni che potrebbero generare eccezioni.
E’ importante che ogni membro venga inizializzato in modo da evitare qualsiasi ambiguità
sul suo valore, soprattutto se si tratta di un puntatore. Questa é una regola che, se
rispettata, potrebbe risparmiare parecchio tempo in fase di debugging. Infatti non vi è
alcuna garanzia che il compilatore inizializzi a zero l’area di memoria in cui viene allocato
un oggetto. Ad esempio, un puntatore non inizializzato potrebbe contenere un indirizzo
che non corrisponde ad alcun oggetto in memoria, oppure si riferisce ad un oggetto che è
giá stato deallocato.
Inoltre, si deve evitare di usare opzioni del compilatore che azzerano automaticamente i
membri di un oggetto. E’ molto meglio che questa operazione sia resa esplicita all’interno
del costruttore.
L’unico caso in cui è possibile omettere l’inizializzazione è quando esiste un costruttore di
default.
class Bitmap{
public:
...
virtual Build();
protected:
...
};
Bitmap::Bitmap(int width,
int height)
: mWidth( width),
mHeight( height)
{
Build();
}
6.2 Distruttori
I distruttori devono essere dichiarati con il specificatore virtual. In questo modo abbiamo la
sicurezza l’oggetto che l'oggetto sia distrutto correttamente anche se viene distrutto da un
puntatore alla classe base.
class Base {
public:
Base();
~Base();
};
Per garantire che venga invocato anche il distruttore di Derived è necessario utilizzare lo
specificatore virtual:
class Base {
public:
Base();
virtual ~Base();
};
Infine, anche all’interno dei distruttori non dovrebbero comparire chiamate a metodi
virtuali.
Una delle peculiarità piú importanti delle classi è sicuramente l’incapsulamento dei dati. I
membri di una classe dovrebbero sempre essere dichiarati private o protected.
Quindi, la cosa migliore da fare é creare un'interfaccia, ovvero, serie di metodi del tipo
Get/Set che regoli l’accesso ai membri sia per la lettura, che per la modifica.
Molto spesso, chi arriva dal C si chiede quale sia l’utilità di dichiarare dei membri privati o
protected. Questo tipo genere di approccio permette di aver maggiore controllo e
sicurezza sia per quanto riguarda i diritti di accesso, sia per garantire la coerenza dei dati.
Ecco una dichiarazione che, oltre a non avere nulla di C++ a parte la parola class, non
garantisce l’integrità dei dati e tantomeno la loro validità:
class Bitmap{
public:
int mWidth,
mHeight;
int mSize;
};
Questa classe non fornisce alcuna garanzia sulla validità dei suoi membri, in particolare di
mSize. Una soluzione più sicura potrebbe essere la seguente:
class Bitmap{
public:
int GetSize();
private:
int mWidth,
mHeight;
int mSize;
};
int
Bitmap::GetSize()
{
return mSize;
}
oppure:
int
Bitmap::GetSize()
{
return mWidth*mHeigh;
}
I due metodi sono equivalenti ma se mSize fosse dichiarato public, chiunque potrebbe
accedere e modificare il suo valore. Inoltre, se non ci fosse il metodo GetSize() e si
decidesse di calcolare il valore invece di tenerlo come membro, ció richiederebbe una
modifica all’interfaccia della classe, con conseguente modifica del codice da parte
dell’utente.
Inoltre, utilizzando dei metodi per richiedere informazioni relative ad un oggetto, si
nasconde l’implementazione in modo del tutto trasparente dal punto di vista
dell’utilizzatore, al quale non interessa come viene svolta una determinata operazione ma
solo che la esegua correttamente.
class Bitmap{
public:
int GetSize();
protected:
int mSize;
};
Il metodo GetSize non effettua alcuna modifica ma non vi è alcuna garanzia che nella sua
implementazione non vengano fatte modifiche ai membri della classe.
class Bitmap{
public:
int GetSize() const;
protected:
int mSize;
};
Ad esempio:
class Bitmap{
public:
int GetSize() const {
mSize = mWidth*mHeight; // assegnazione non permessa
return mSize;
}
protected:
int mWidth;
int mHeight;
int mSize;
};
I membri statici possono essere la soluzione per molti problemi ma attenzione a farne un
uso eccessivo.
Devono essere usati principalmente per condividere dati tra oggetti della stessa classe e
non semplicemente come un metodo alternativo per dichiarare variabili globali!
Inoltre, devono essere limitati i casi in cui un oggetto deve accedere ai membri statici di
un’altra classe. Se ció avviene molto frequente, cominciate a chiedervi se quei membri
non debbano essere dichiarati dentro alla classe che li usa.
Non bisogna mai fare assunzioni sull’ordine di inizializzazione dei membri statici. Nel caso
in cui non si possa fare altrimenti è possibile verificare se il compilatore permette di
definire una propria procedura in cui vengono fatte le inizializzazioni dei membri statici.
Non tutti i compilatori supportano questa opzione.
Chi viene a conoscenza per la prima volta di questa caratteristica del linguaggio, spesso
viene preso dalla voglia di utilizzare gli operatori in sostituzione dei metodi tradizionali. In
effetti, con gli operatori si possono scrivere espressioni complesse ma può accadere che
questo vada a scapito della leggibilità.
A questo proposito non ci sono regole particolari ma un solo consiglio: l’overloading degli
operatori deve essere utilizzato per migliorare la leggibilità non il contrario. Se quello che
si ottiene è un codice di difficile interpretazione allora non conviene ridefinire gli operatori.
7 Portabilità
La portabilitá è una caratteristica del codice che spesso non viene considerata nelle fasi
iniziali di progettazioni o che, comunque, viene tralasciata per risparmiare tempo.
Il problema è che, spesso, il tempo risparmiato inizialmente viene perso quando si decide
di dare il porting per un'altra piattaforma. Bisogna considerare che è molto piú costoso
fare il porting in fase avanzata, piuttosto di progettare il codice fin dall’inizio in modo da
poterlo convertire facilmente per un’altra piattaforma.
Lo scopo di questa guida non è sicuramente di spiegare in dettaglio le questioni
riguardanti la portabilità ma vediamo alcuni semplici consigli che possono far risparmiare
molto tempo in fase avanzata di progetto.
Mettere a disposizione una serie di tipi di base che chiunque potrà utilizzare è
sicuramente un metodo per rendere il codice meno indipendente dalla piattaforma:
typedef signed char SInt8;
typedef signed short SInt16;
typedef signed long SInt32;
in questo modo è possibile effettuare modiche ai tipi di base senza dover cambiare tutto il
codice che li utilizza.
Ad esempio, se volessimo definire un tipo Real come:
typedef float Real;
senza effettuare altre modifiche al codice, evitando di fare un find & replace in tutti i
sorgenti.
Le definizioni condizionali sono utili a scegliere alcune proprietà che influenzano l’intera
compilazione. Infatti, quando si scrive codice portabile si devono modificare le parti di
codice che dipendono dalla piattaforma ed è molto utile poter utile definire alcuni
parametri che dipendono da:
• processore
• sistema operativo
• compilatore
enum {
False = 0,
True = 1
};
#endif
in questo caso si evita che il tipo Boolean venga definito se il compilatore lo fornisce giá
nelle sue librerie.
8 Ottimizzazioni
8.1 Assembler
Codice assembler dovrebbe essere evitato, sia per quanto riguarda la portabilità, sia
perché non è sicuro che riscrivendo una sequenza di istruzioni da C/C++ in Assembler si
ottenga un miglioramento di prestazioni. In ogni caso, bisogna tenere sempre presente
che difficilmente riusciremo ad ottimizzare il codice meglio del compilatore che, nella
maggior parte dei casi ha delle opzioni specifiche riguardanti l’ottimizzazione.
Quindi, il consiglio è quello di fare ottimizzazioni di “alto livello”, lasciando fare le altre al
compilatore. Ad esempio, dovendo velocizzare le prestazioni di gioco con visualizzazione
in 3d è preferibile “lavorare” sugli algoritmi per determinare quali sono gli oggetti visibili,
piuttosto che scrivere in assembler altre porzioni di codice.
In generale, questi consigli vanno molto bene per chi sviluppa per PC e devono invece
essere rivisti per quanto riguarda le console e sistemi embedded.
8.2 Inlining
Il c++ ha delle caratteristiche che possono essere utili per rendere il codice più
performante. La più utilizzata è sicuramente l’inlining, la seconda invece sono i template.
L’inlining può essere considerato come un’evoluzione delle macro ma con dei vantaggi
molto importanti. Il primo è quello di non avere function-call overhead, quindi di garantire
le stesse prestazioni delle macro e il secondo è il fatto di essere type-safe.
Anche per quanto riguarda queste particolarità del linguaggio bisogna fare attenzione a
non usarlo dove non è necessario, infatti, è possibile che del codice inlined o unrolled
risulti meno performante. Ciò è dovuto al meccanismo di caching delle porzioni di codice
eseguite più frequentemente dal processore. Il codice in versione unrolled occupa più
spazio nella cache del processore, mentre quello in versione compatta potrebbe stare
tutto nella cache senza dover richiedere continuamente che venga sostituito. A questo
proposito è consigliabile l’utillizzo di un profiler per verificare se, nel caso specifico, è
conveniente usare l’inlining.
8.3 Template
9 Varie
9.1 Costanti
switch (value) {
case kButton :
...
break;
case kPicture :
...
break;
case kEditField :
...
break;
default :
cout << “Unknown value” << endl;
break;
}
#define NETWORK_NODE 0
#define ELECTRICAL_NODE 1
#define ROAD_NODE 2
#define OBJECT_NODE 3
#define BUILDING_NODE 4
In questo modo il programmatore non si deve più preoccupare dell’univocità delle costanti,
infatti, questo compito è demandato al compilatore. Inoltre, è possibile utilizzare il tipo
NodeType per il passaggio di parametri ai metodi o come valore di ritorno, evitando errori
a tempo di compilazione.
La definizione di costanti è uno dei due principali utilizzi del #define. Il secondo riguarda le
macro.
Ad esempio, dichiarazioni del tipo:
#define FOR_EACH_ITEM( l) for( list<int>::iterator iter(l.begin()); \
iter != l.end(); iter++)
FOR_EACH_ITEM(myList){
// do something
...
}
Devono essere assolutamente evitate, infatti, il codice potrebbe risultare poco chiaro e
soprattutto lo rende difficilmente debuggabile.
Anche dichiarazioni come:
#define abs(x) ( ((x) < 0) ? -(x) : (x) )
oppure, nel caso in cui non si voglia limitare la funzione agli int, si possono usare i
template:
template<class T>
T abs(const T& x) {
return x < 0 ? -x : x;
}
In C++ non ha più senso usare le #define per motivi prestazionali, pertanto devono essere
evitate.
Nel caso in cui la dimensione di un array sia conosciuto a compile-time e sia la new che
delete vengono chiamate nello stesso scope, allora è preferibile allocare l’array nello
stack.
situazioni come:
{
char* temp = new char[10];
… // do something
delete [] temp;
}
Stesso discorso per i valori di ritorno di una funzione. Anche in questo caso, utilizzando i
reference, si evita la costruzione di un oggetto temporaneo che viene poi immediatamente
distrutto.
9.6 Namespace
namespace gfx {
// my declarations/implementations
// ...
// ...
9.7 Cast
Il cast dovrebbe essere limitato il più possibile e nei casi in cui non se ne può fare a meno
bisogna usare gli operatori di cast forniti dal C++:
• static_cast
• dynamic_cast
• reinterpret_cast
• const_cast
Lo static_cast equivale al classico cast del linguaggio c è deve essere usato ogni qual
volta il tipo tra parentesi angolari si conosce a compile-time. Attenzione però, codice in cui
sono presenti molti cast, potrebbe essere sintomo di un errore in fase di design. Il cast
non è affatto indispensabile è nella maggior parte dei casi può essere evitato.
Personalmente lo considero come un metodo provvisorio per accedere ad una serie di
dati in modo veloce.
9.8 RTTI
Window(...);
virtual ~Window();
Dialog(...);
virtual ~ Dialog();
if (pWnd->GetClassId() == Dialog::kClassId)
cout << “This object is a Dialog” << endl;
else
cout << “This object is a Window” << endl;
I puntatori a funzioni sono molto utili per poter eseguire un particolare procedura in
determinate situazioni, di solito in seguito al verificarsi di una particolare condizione. Si
puó considerare come uno degli strumenti piú potenti che metta a disposizione il
linguaggio C. Il C++, oltre a mantenere questa possibilità, permette anche di definire delle
classi-funzioni.
Prima soluzione:
class Func{
public:
Func(){
}
virtual ~ Func(){
}
virtual ~ Func(){
}
Nel secondo caso, la classe Func non può essere istanziata, infatti, possiede l’ operatore
() che è un metodo virtuale puro. In questo modo è obbligatorio ridefinire l’operatore ()
nella classe derivata:
Le istruzioni goto, break e return possono essere considerate come dei jump in assembly
ma a prescindere dalla loro implementazione che potrebbe variare da un compilatore
all’altro, una cosa certa è che contribuiscono notevolmente a rendere il codice poco
leggibile.
L’istruzione goto non deve essere mai utitlizzata, in nessun caso. Perché ridursi a
programmare in QBasic usando i costrutti del C++?
Anche il break è una sorta di goto e deve essere usato solo all’interno degli switch.
Al contrario del goto, il ritorno da procedura, return, è inevitabile e non se ne può fare a
meno ma bisogna fare molta attenzione a non abusarne. Infatti, codice con molti return
diventa difficile da seguire e da modificare.
Ad esempio:
pObject->DoSomething();
}
potrebbe diventare:
• Attenzione a distinguere sempre tra delete e delete []. In generale, il compilatore non
segnala alcun errore nel caso in cui si tenti di deallocare un array mediante la delete
semplice.
• Limitare le dichiarazioni extern.
• Evitare l’uso di variabili globali.
• Usare lo static_cast<> invece del classico cast.
• Non usare la memset() per inizializzare i membri di una classe
• Non usare la memcpy() all’interno degli operatori di assegnamento
• Usare i reference quando è possibile. I puntatori quando non se ne può fare a meno.
• Invece di usare il postincremento è preferibile usare il preincremento. Infatti, il primo è
fatto in funzione del secondo.
• Evitare l’uso dell’istruzione continue
• Mai fare assegnamenti all’interno di una condizione. Es.: if (result =
CreateBitmap()) { }
10 Conclusioni
Credo che sia praticamente impossibile programmare in C++ alla perfezione, ma una
cosa è usare il C++ come il C, ognuno è libero di farlo e di programmare con le tecniche
che conosce meglio. Un altra cosa, invece è programmare utilizzando alcune particolarità
del linguaggio solo perché sono disponibili. Infatti, è impensabile che un programmatore C
appena passato al C++ si metta subito ad usare i template. Insomma, come per tutte le
cose ci vuole un po' di tempo. Quindi il consiglio di scrivere usando i costrutti e le tecniche
che si conoscono meglio cercando di ottenere codice “pulito” e documentato. In questo
modo il codice risulta manutenibile e può essere modificato successivamente senza dover
riscrivere tutto.
Spero che questa guida possa essere utile quantomeno per programmare meglio in modo
da ottenere un codice pulito e, soprattutto, manutenibile. Infine, vorrei dare un ultimo
consiglio: esistono infiniti stili di programmazione, ognuno con le proprie regole e
fissazioni ma la cosa più importante è che quando si scrive del codice vi sia quantomeno
un criterio.
11 Links Utili
1. C++ Faq Lite
4. Doxygen Homepage
5. How to force MS Visual C++ to use .cc as extension for C++ files
12 Bibliografia
1. John Stenersen, “A Case for Code Review”, Gamasutra Features Articles (2000)