[ Suggerimenti
] [ Soluzioni
degli Esercizi] [ Volume
2 ] [ Newsletter
Gratuita ]
[ Seminari
] [ Seminari
su CD ROM ] [ Consulenza]
[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
traduzione italiana e adattamento a cura di Alberto Francia
Quest'appendice è un insieme di suggerimenti per la programmazione in C++. Sono stati messi insieme nel corso della mia esperienza di insegnamento e
di programmazione, nonché dai consigli di amici come Dan Saks (coautore con Tom Plum di C++ Programming Guidelines, Plum Hall, 1991), Scott Meyers (autore di Effective C++, 2nd editioN, Addison-Wesley, 1998), e Rob Murray (autore di C++ Strategies & Tactics, Addison-Wesley, 1993). Inoltre, molti suggerimenti sono direttamente tratti dalle pagine di Thinking in C++.
Prima fatelo funzionare, poi rendetelo veloce. Questo è vero anche se siete certi che un frammento di codice sia realmente importante e che diverrà il collo di bottiglia principale del vostro sistema. Non fatelo. Prima di tutto cercate di ottenere un sistema funzionante con un progetto il più semplice possibile. Soltanto dopo, se non è abbastanza veloce, analizzatelo. Quasi sempre scoprirete che il vero problema non è il "vostro" collo di bottiglia. Risparmiate tempo per le cose veramente utili.
L'eleganza paga sempre. Non è un'attività frivola. Avrete in mano un programma non solo più facile da compilare e testare, ma anche da comprendere e da mantenere: ed è qui che si ritrova il valore economico. Potrebbe servire un po' di esperienza prima di credere a questo fatto, in quanto potrebbe sembrare che, mentre cercate di rendere elegante un frammento di codice, non siate produttivi. La produttività emergerà quando il codice si integrerà perfettamente nel vostro sistema, ed ancor più quando il codice o il sistema verrà modificato.
Ricordatevi del principio "divide et impera". Se il problema che state affrontando è troppo complesso, cercate di immaginare quali potrebbero essere le operazioni basilari del programma, se esistesse un essere soprannaturale che si occupasse delle parti più difficili. Quest'essere soprannaturale è un oggetto - scrivete il codice che utilizza quell'oggetto, quindi studiate l'oggetto ed incapsulate le sue parti complicate all'interno di altri oggetti, e così via.
Non riscrivete automaticamente in C++tutto il vostro codice C già esistente, a meno che non dobbiate variarne le funzionalità in maniera significativa (in pratica, se non è guasto, è inutile ripararlo). Ricompilare il codice C in C++ è invece un'attività utile, perché potrebbe rivelare dei bug nascosti. Comunque, prendere del codice C che funziona bene e riscriverlo in C++ potrebbe non essere il modo migliore per passare il vostro tempo, a meno che la versione C++, essendo una classe, non offra molte più possibilità di riutilizzo.
Se avete un grosso blocco di codice C che ha bisogno di modifiche, cominciate ad isolarne le parti che non verranno modificate, eventualmente inglobandole in una "classe API" come metodi statici. Successivamente, focalizzate la vostra attenzione sul codice che verrà modificato, ristrutturandolo in classi in modo da rendere agevoli le modifiche man mano che la vostra manutenzione procede.
Tenete ben distinti il creatore della classe dal suo utilizzatore (il programmatore client). Chi utilizza la classe è il "cliente", e non ha bisogno né vuole sapere cosa accade dietro le quinte. Chi crea la classe dev'essere l'esperto di progettazione di classi e deve scriverla in modo che possa essere usata anche dal più principiante dei programmatori, continuando a comportarsi in maniera robusta nell'applicazione. L'utilizzo di una libreria è semplice soltanto se è trasparente.
Quando create una classe, usate i nomi più chiari possibile. Il vostro obiettivo è quello di rendere l'interfaccia di programmazione concettualmente semplice. Cercate di rendere i vostri nomi talmente chiari da rendere superflui i commenti. A tal fine, sfruttate l'overloading delle funzioni e gli argomenti di default per creare un'interfaccia intuitiva e facile da usare.
Il controllo dell'accesso consente a voi (creatori della classe) di fare in futuro le più grandi modifiche possibili senza danneggiare il codice client nel quale la classe è utilizzata. In questa prospettiva, mantenete tutto il più private possibile, e rendete public solo l'interfaccia della classe, utilizzando sempre le funzioni anziché i dati. Rendete i dati public solo quando siete costretti. Se gli utilizzatori di una classe non hanno bisogno di chiamare una funzione, dichiaratela private. Se una parte della vostra classe deve restare visibile agli eredi come protected, fornite un'interfaccia a funzioni piuttosto che esporre direttamente i dati. In questo modo, le modifiche di implementazione avranno un impatto minimo sulle classi derivate.
Non commettete l'errore di bloccarvi durante l'analisi. Ci sono certe cose delle quali non vi renderete conto finché non inizierete a scrivere codice e ad avere una specie di sistema funzionante. Il C++ ha al proprio interno delle barriere protettive; lasciate che lavorino per voi. Gli errori che commetterete in una classe o in un insieme di classi non distruggeranno l'integrità dell'intero sistema.
La vostra analisi ed il vostro progetto devono produrre, quantomeno, le classi del vostro sistema, le loro interfacce pubbliche, e le loro relazioni con le altre classi, in particolare con le classi base. Se il vostro metodo di progetto produce più di questo, chiedetevi se tutto ciò che viene prodotto dalla vostra tecnica serve a qualcosa durante il ciclo di vita di un programma. Se la risposta è no, mantenerlo avrà un costo per voi. I membri dei team di sviluppo tendono a non mantenere nulla che non contribuisca alla loro produttività; questo è un dato di fatto che molte tecniche di progetto non prendono in considerazione.
Prima scrivete il codice di test (prima di scrivere la classe), e tenetelo insieme alla classe. Rendete automatica l'esecuzione dei vostri test per mezzo di un makefile o di uno strumento simile. In questo modo, qualunque modifica può essere controllata automaticamente lanciando il codice di test, e gli errori verranno immediatamente scoperti. Sapendo di avere la rete di sicurezza dell'ambiente di test, sarete più propensi ad effettuare modifiche consistenti quando ne sentirete il bisogno. Ricordate che i maggiori miglioramenti nei linguaggi di programmazione vengono dai controlli interni forniti da controllo del tipo, gestione delle eccezioni e così via, ma queste caratteristiche servono fino ad un certo punto. Dovete arrivare in fondo alla strada che porta alla creazione di sistemi robusti introducendo i test che verificano le caratteristiche specifiche della vostra classe o del vostro programma.
Prima scrivete il codice di test (prima di scrivere la classe) in modo da controllare che il progetto della vostra classe sia completo. Se non siete in grado di scrivere il codice di test, significa che non sapete che aspetto presenta la vostra classe. Inoltre, il semplice atto di scrivere il codice di test farà spesso emergere caratteristiche aggiuntive o vincoli di cui avete bisogno nella classe - queste caratteristiche e questi vincoli non appaiono sempre durante l'analisi ed il progetto.
Ricordate una regola fondamentale dell'ingegneria del software [1]: Tutti i problemi di progetto del software possono essere semplificati introducendo un ulteriore livello di dereferenziamento concettuale. Quest'idea sta alla base dell'astrazione, la caratteristica primaria della programmazione orientata agli oggetti.
Rendete le classi le più atomiche possibile; in altri termini, date ad ogni classe un unico scopo chiaro. Se le vostre classi o il vostro progetto del sistema diventano troppo complicati, suddividete le classi complesse in classi più semplici. Il segnale più ovvio di questo fatto è proprio la stessa dimensione: se una classe è grande, c'è la possibilità che stia facendo troppo e che andrebbe divisa.
Guardatevi dalle definizioni dei metodi lunghe. Una funzione lunga e complicata è difficile e costosa da mantenere, e probabilmente sta cercando di fare troppo da sola. Se trovate una funzione del genere, significa che, quantomeno, andrebbe suddivisa in alcune funzioni più piccole. Potrebbe anche suggerire la creazione di una nuova classe.
Guardatevi dalle liste di argomenti lunghe. Le chiamate di funzione diventano difficili da scrivere, leggere e mantenere. Piuttosto, cercate di spostare il metodo in una classe alla quale sia (più) adatto, e/o a passargli un oggetto come parametro.
Non ripetetevi. Se un pezzo di codice compare in molte funzioni nelle classi derivate, spostate quel codice in un'unica funzione nella classe base e chiamatelo dalle funzioni delle classi derivate. Non solo risparmierete spazio, ma consentirete un'agevole propagazione delle modifiche. Potete utilizzare una funzione inline per l'efficienza. Talvolta, la scoperta di questo codice comune porta considerevoli benefici alla funzionalità della vostra interfaccia.
Guardatevi dalle istruzioni switch o dagli if-else concatenati. Tipicamente, questo è un indicatore della programmazione di tipo type-check, che significa che state scegliendo quale codice eseguire in base ad un qualche genere di informazione sul tipo (il tipo esatto potrebbe non esservi immediatamente chiaro). Solitamente, potete rimpiazzare questo genere di codice sfruttando ereditarietà e polimorfismo; la chiamata ad una funzione polimorfica eseguirà il controllo di tipo per voi, e consentirà un estensibilità più affidabile e semplice.
Al momento del progetto, cercate e separate le cose che cambiano da quelle che restano uguali. In altre parole, individuate gli elementi del sistema che potreste voler modificare senza dover riprogettare tutto, quindi incapsulate quegli elementi nelle classi. Potete avere maggiori dettagli su questo concetto nel capitolo dedicato ai Design Pattern nel Volume 2 di questo libro, disponibile presso www.BruceEckel.com.
Guardatevi dalla varianza. Due oggetti semanticamente differenti potrebbero avere identiche funzioni, o responsabilità, e c'è una naturale tentazione di cercare di renderla una sottoclasse dell'altra, con l'unico scopo di trarre benefici dall'ereditarietà. Questa tecnica si chiama varianza, ma non c'è un valido motivo per forzare una relazione superclasse/sottoclasse quando questa non esiste. Una soluzione migliore sarebbe creare una classe base generale che genera per entrambe un'interfaccia - richiede un po' più spazio ma vi consente ancora di trarre vantaggio dall'ereditarietà e probabilmente di fare interessanti scoperte sul progetto.
Guardatevi dalle limitazioni nell'ereditarietà. I progetti più puliti aggiungono nuove funzionalità a quelle ereditate. Un progetto di cui diffidare rimuove le vecchie funzionalità durante l'eredità senza aggiungerne altre. Ma le regole sono fatte per essere infrante, e se state lavorando con una vecchia libreria di classi potrebbe essere più efficiente restringere una classe esistente nelle sue sottoclassi, piuttosto che ristrutturare la gerarchia in modo che la vostra nuova classe si vada ad inserire dove dovrebbe, al di sopra della vecchia classe.
Non estendete le funzionalità fondamentali nelle sottoclassi. Se un elemento dell'interfaccia è fondamentale per una classe si dovrebbe trovare nella classe base, e non essere aggiunto nel corso delle derivazioni. Se state aggiungendo dei metodi tramite l'eredità, forse dovreste ripensare il progetto.
Meno è più. Iniziate da un'interfaccia minimale per la classe, semplice e piccola quanto basta per risolvere il vostro problema corrente, ma non cercate di anticipare tutti i modi nei quali la vostra classe potrebbe essere usata. Al momento del suo utilizzo, scoprirete il modo nel quale dovrete espandere l'interfaccia. Comunque, una volta che la classe è in uso, non potete modificarne l'interfaccia senza disturbare il codice client. Se avete bisogno di aggiungere più funzioni, va bene; non creerà problemi al codice, se non la necessità di una ricompilazione. Ma anche se i nuovi metodi rimpiazzano le funzionalità di quelle vecchie, lasciate da sola l'interfaccia già esistente (se volete, potete combinare le funzionalità nell'implementazione sottostante). Se avete bisogno di espandere l'interfaccia di una funzione esistente aggiungendo nuovi argomenti, lasciate gli argomenti già esistenti nel loro ordine, ed assegnate dei valori di default a tutti quelli nuovi; in questo modo, non creerete problemi a nessuna chiamata già esistente a quella funzione.
Leggete le vostre classi ad alta voce per assicurarvi che siano logiche, riportando la relazione tra classe base e classe derivata come "è un", e quella tra classe ed oggetto membro come "ha un".
Al momento di scegliere fra eredità e composizione, chiedetevi se avete bisogno di fare un upcast al tipo base. In caso negativo, privilegiate la composizione (oggetti membri) all'eredità. Ciò può eliminare la necessità percepita di usare l'eredità multipla. Se ereditate, gli utenti penseranno che prevediate che facciano upcast.
Talvolta, avete bisogno di ereditare per poter accedere ai membri protected della classe base. In questo modo potreste pensare di avere bisogno dell'eredità multipla. Se non avete bisogno di upcast, dapprima derivate una nuova classe per eseguire l'accesso protected. In seguito, rendete questa nuova classe un oggetto membro di qualunque classe che abbia bisogno di utilizzarlo, piuttosto che ereditarlo.
Tipicamente, una classe base verrà utilizzata principalmente per creare un'interfaccia alle classi derivate da lei. Pertanto, quando create una classe base, create tendenzialmente le funzioni membro come virtuali pure. Anche il distruttore può essere virtuale puro (per forzare gli eredi a sovrascriverlo esplicitamente), ma ricordate di dare al distruttore un corpo, perché tutti i distruttori di una gerarchia vengono sempre chiamati.
Quando inserite una funzione virtual in una classe, rendete virtual tutte le funzioni di quelle classe, ed inserite un distruttore virtual. Questo approccio previene sorprese nel comportamento dell'interfaccia. Iniziate a rimuovere la parola chiave virtual solo quando avete problemi di efficienza ed il vostro analizzatore vi ha suggerito questa soluzione.
Utilizzate le variabili membro per le variazioni di valore, e le funzioni virtual per le variazioni di comportamento. In altre parole, se trovate una classe che utilizza variabili di stato unitamente a funzioni membro che modificano il loro comportamento in base a quelle variabili, probabilmente dovreste riprogettarla esprimendo le differenze di comportamento tramite sottoclassi e funzioni virtual soprascritte.
Se dovete fare qualcosa di non portabile, create un astrazione per quel servizio ed inseritelo in una classe. Questo livello ulteriore di redirezione impedisce alla non portabilità di essere distribuita in tutto il programma.
Evitate l'eredità multipla. Serve a tirarvi fuori da situazioni spiacevoli, in particolare per riparare interfacce di classi sulle quali non avete il controllo (vedete il Volume 2). Dovreste essere programmatori esperti prima di progettare eredità multiple per il vostro sistema.
Non usate eredità private. Sebbene sia prevista dal linguaggio e talvolta sembri essere funzionale, introduce notevoli ambiguità quando è combinata all'identificazione run-time del tipo. Create un oggetto membro private, piuttosto che utilizzare l'eredità privata.
Se due classi sono in qualche maniera funzionalmente associate tra di loro (ad esempio come i contenitori e gli iteratori), cercate di rendere una delle due una classe public e friend annidata nell'altra, così come la Libreria Standard C++ fa con gli iteratori dentro i contenitori (esempi di questa tecnica sono mostrati nella parte finale del Capitolo 16). In questo modo non solo viene enfatizzata l'associazione fra le due classi, ma, anidandolo all'interno di un'altra classe, si consente al nome della classe di essere riutilizzato. La Libreria Standard C++ fa questo definendo una classe iterator annidata in ciascuna classe contenitore, fornendo così ai contenitori un'interfaccia comune. L'altra ragione per la quale potreste voler annidare una classe è come parte di un'implementazione private. In questo caso, l'annidamento è utile più per nascondere l'implementazione che per sottolineare l'associazione fra le classi o prevenire l'inquinamento del namespace come descritto sopra.
L'overloading degli operatori è soltanto "zucchero sintattico": un modo diverso per chiamare una funzione. Se l'overloading di un operatore non rende l'interfaccia della classe più chiara e semplice da usare, non fatelo. Per una classe create solo un operatore per la conversione automatica di tipo. In generale, seguite le linee guida ed il formato descritto nel Capitolo 12 quando effettuate l'overloading degli operatori.
Non preoccupatevi di un'ottimizzazione prematura. È pura follia. In particolare, non preoccupatevi di scrivere (o evitare) le funzioni inline, di rendere non virtual certe funzioni, o di forzare il codice ad essere efficiente quando state appena costruendo il sistema. Il vostro scopo principale è di verificare il progetto, a meno che lo stesso richieda una certa efficienza.
Di norma, evitate che sia il compilatore a creare per voi costruttori, distruttori ed operator=. I progettisti delle classi dovrebbero sempre dire esattamente che cosa la classe dovrebbe fare e mantenere la classe completamente sotto controllo. Se non volete avere un costruttore di copia o un operator=, dichiarateli come private. Ricordate che, se create un qualunque costruttore, impedirete al costruttore di default di essere sintetizzato.
Se la vostra classe contiene puntatori, per farla funzionare correttamente dovete creare il costruttore di copia, operator= ed il distruttore.
Quando scrivete il
costruttore di copia per una classe derivata, ricordatevi di richiamare
esplicitamente il costruttore di copia della classe base (anche nelle
versioni che hanno un oggetto come membro) (Vedete il Capitolo 14). Se
non lo fate, per la classe base (o per l'oggetto membro) verrà richiamato
il costruttore di default, e probabilmente non è quello che volete. Per
chiamare il costruttore di copia della classe base, passategli l'oggetto
derivato dal quale state copiando:
Derived(const Derived& d) : Base(d)
{ // ...
Derived& operator=(const Derived&
d) {
Base::operator=(d);
Se avete bisogno di minimizzare le ricompilazioni durante lo sviluppo di un progetto esteso, utilizzate la tecnica della classe di gestione/gatto Cheshire mostrata nel Capitolo 5, e rimuovetela solo se l'efficienza a runtime costituisce un problema.
Evitate il preprocessore. Usate sempre const per sostituire i valori e le funzioni inline per le macro.
Mantenete gli scope più piccoli possibile, in modo che la visibilità e la vita dei vostri oggetti siano le più ridotte possibile. In questo modo, diminuisce la possibilità di utilizzare un oggetto in un contesto sbagliato e di nascondere un bug difficile da trovare. Per esempio, supponete di avere un contenitore ed un frammento di codice che itera su di lui. Se copiate quel codice per usarlo con un altro contenitore, potreste trovarvi accidentalmente ad utilizzare la dimensione del vecchio contenitore come limite superiore per quello nuovo. Se, comunque, il vecchio contenitore è esterno allo scope, l'errore verrà scoperto al momento della compilazione.
Evitate le variabili globali. Cercate sempre di inserire i dati all'interno delle classi. È più probabile imbattersi in funzioni globali piuttosto che in variabili globali, sebbene potreste rendervi conto in seguito che una funzione globale troverebbe una collocazione più consona come metodo static di una classe.
Se avete bisogno
di dichiarare una classe o una funzione di una libreria, fatelo sempre
utilizzando un file header. Per esempio, se volete creare una funzione
per scrivere su di un ostream, non dichiarate
mai ostream per conto vostro utilizzando una
specificazione di tipo incompleta come questa,
class ostream;
#include <iostream>.
Quando scegliete
il tipo restituito dall’overloading di un operatore, considerate che cosa
accadrebbe se le espressioni venissero concatenate assieme. Restituite
una copia di un reference ad un lvalue (return *this
) in modo
che possa essere utilizzato in espressioni concatenate (A=B=C
).
Quando definite operator=, ricordatevi di x
= x
.
Quando scrivete una funzione, come prima scelta passatele gli argomenti come const reference. Fintantoché non dovete modificare l’oggetto passato, questa è la scelta migliore, in quanto possiede la semplicità del passaggio per valore ma non richiede costose operazioni di costruzione e distruzione per creare un oggetto locale, cosa che accade quando un parametro viene passato per valore. Normalmente, non dovreste preoccuparvi eccessivamente delle problematiche di efficienza quando progettate e costruite il vostro sistema, ma quest’abitudine è di certo un punto vincente.
Siate consapevoli
degli oggetti temporanei. Quando cercate le prestazioni, controllate la
creazione di oggetti temporanei, soprattutto in presenza di overload degli
operatori. Se i vostri costruttori e distruttori sono complicati, il costo
di creare e distruggere oggetti temporanei può essere elevato. Quando
restituite un oggetto da una funzione, cercate sempre di costruire l’oggetto
“in loco” con una chiamata al costruttore nell’istruzione return:
return MyType(i, j);
MyType x(i, j);
return x;
Quando create i costruttori, prendete in considerazione le eccezioni. Nella migliore delle ipotesi, il costruttore non farà nulla che lanci un’eccezione. Nello scenario appena peggiore, la classe sarà composta ed erediterà solo da classi robuste, che quindi si puliranno automaticamente se un’eccezione viene lanciata. Se dovete avere puntatori “scoperti”, siete responsabili di catturare le vostre eccezioni e quindi di deallocare tutte le risorse puntate prima di lanciare un eccezione nel vostro costruttore. Se un costruttore deve fallire, il modo migliore per farlo è lanciare un’eccezione.
Nei vostri costruttori fate solo il minimo necessario. In questo modo, non solo produrrete un costo minimo per le chiamate al costruttore (molte delle quali potrebbero non essere sotto il vostro controllo), ma è anche meno probabile che i vostri costruttori lancino eccezioni o creino problemi.
La responsabilità del distruttore è di rilasciare le risorse allocate durante l’intera vita dell’oggetto, non solo durante la costruzione.
Utilizzate le gerarchie di eccezioni, preferibilmente derivandole dalla gerarchia di eccezioni standard del C++ ed annidate come classi public all’interno della classe che lancia l’eccezione. La persona che cattura l’eccezione può così catturare i tipi specifici di eccezione, seguiti dal tipo base. Se aggiungete nuove eccezioni derivate, il codice client esistente catturerà ancora l’eccezione attraverso il tipo base.
Lanciate le eccezioni per valore, e catturatele per riferimento. Lasciate che sia il meccanismo di gestione delle eccezioni ad occuparsi della gestione della memoria. Se lanciate puntatori ad oggetti eccezione che sono stati creati sull’heap, chi la cattura deve sapere di doverla distruggere, e questo è un comportamento scorretto. Se catturate le eccezioni per valore, provocate ulteriori costruzioni e distruzioni; peggio ancora, le porzioni derivate dei vostri oggetti eccezione potrebbero andare perdute nel corso dell’upcast per valore.
Non create dei vostri propri class template, a meno che non vi siate costretti. Prima guardate nella Libreria Standard C++, quindi rivolgetevi ai venditori che creano strumenti di tipo special-purpose. Acquistate dimestichezza con il loro uso ed incrementerete notevolmente la vostra produttività.
Quando create i template, cercate il codice che non dipende dal tipo e collocatelo in una classe base esterna al template per prevenire un inutile rigonfiamente del codice. Utilizzando l’ereditarietà o la composizione, potete creare template nei quali la maggior parte del codice dipende dal tipo ed è quindi essenziale.
Non utilizzate le funzioni <cstdio>, come printf(). Piuttosto, imparate ad utilizzare gli iostream; sono funzioni type-safe e type-extensible, e nettamente più potenti. Il vostro sforzo sarà regolarmente ricompensato. In generale, usate sempre le librerie C++ piuttosto delle librerie C.
Evitate i tipi nativi del C. Sono supportati dal C++ solo per ragioni di compatibilità all’indietro, ma sono molto meno robusti delle classi C++, quindi il tempo dedicato alla ricerca dei bug aumenta.
Tutte le volte che usate i tipi nativi come globali o automatici, non definiteli fintantoché non potete anche inizializzarli. Definite la variabili una per riga, ciascuna con la propria inizializzazione. Quando definite i puntatori, posizionate la ‘*’ vicino al nome del tipo. Potete farlo senza pericolo, se definite una variabile per riga. Questo stile tende a confondere meno l’utente.
Garantite che l’inizializzazione abbia luogo in tutti gli aspetti del vostro codice. Eseguite tutte le inizializzazioni dei membri nella lista di inizializzazione del costruttore, anche per i tipi nativi (utilizzate chiamate a pseudocostruttori). L’utilizzo della lista di inizializzazione del costruttore è spesso più efficiente quando si inizializzano gli oggetti membro; in caso contrario viene chiamato il costruttore di default, e finite per chiamare, dopo di lui, altre funzioni membro (probabilmente operator=) per ottenere l’inizializzazione desiderata.
Non utilizzate la
forma MyType a = b;
per definire un oggetto. Questa caratteristica
è una delle maggiori fonti di confusione, perché viene chiamato il costruttore
e non operator=. Per chiarezza, siate sempre
precisi ed utilizzate piuttosto la forma MyType a(b);
. Il
risultato è identico, ma non confonderete le idee agli altri programmatori.
Utilizzate i cast espliciti descritti nel Capitolo 3. Un cast scavalca il sistema normale dei tipi, ed è una fonte potenziale di errori. Dal momento che i cast specifici suddividono il cast “tuttofare” del C in categorie di cast ben definiti, chiunque esegua il debug o mantenga il codice è in grado di trovare facilmente tutti i punti nei quali è più probabile il verificarsi di errori logici.
Perché un programma sia robusto, ogni singolo componente deve essere robusto. Sfruttate tutti gli strumenti offerti dal C++: controllo degli accessi, eccezioni, correttezza delle costanti, controllo del tipo, e così via, e questo in ogni classe che create. In questo modo, quando costruite il vostro sistema, potete muovervi con sicurezza verso il livello di astrazione successivo.
Utilizzate la correttezza delle costanti. Consente al compilatore di evidenziare dei bug che, altrimenti, sarebbero stati infidi e difficili da individuare. Questa abitudine ha bisogno di una certa disciplina e dev’essere utilizzata in maniera consistente in tutte le classi, ma paga.
Sfruttate a vostro vantaggio il controllo degli errori effettuato dal compilatore. Compilate il vostro codice abilitando tutti i warning, e correggete il vostro codice in modo da eliminarli tutti. Scrivete codice che utilizza gli errori ed i warning al momento della compilazione, piuttosto che codice che provochi errori di runtime (ad esempio, non utilizzate liste di argomenti variadic, che impediscono qualsiasi controllo dei tipi). Utilizzate assert() per il debug, ma a runtime usate le eccezioni.
Preferite gli errori di compilazione agli errori di runtime. Cercate di gestire un errore il più vicino possibile al punto in cui si è verificato. È preferibile gestire l’errore in quel punto piuttosto che lanciare un eccezione. Catturate le eccezioni nel gestore più vicino che abbia informazioni sufficienti per gestirle. Fate il possibile col l’eccezione al livello corrente; se in questo modo non risolvete il problema, rilanciatela nuovamente. (Vedete il Volume 2 per maggiori dettagli.)
Se state utilizzando le specifiche delle eccezioni (vedete il Volume 2 di questo libro, scaricabile a http://www.BruceEckel.com, per imparare a gestire le eccezioni) installate una vostra funzione unexpected() utilizzando set_unexpected(). La vostra funzione unexpected() dovrebbe registrare l’errore e rilanciare l’eccezione corrente. In questo caso, se una funzione esistente viene scavalcata e comincia a lanciare eccezioni, avrete a disposizione una traccia di quanto è accaduto e potrete modificare il codice chiamante per gestire l’eccezione.
Create una funzione terminate() (che indica un errore di programmazione) personalizzata per registrare l’errore che ha portato all’eccezione, dopodiché rilasciate le risorse del sistema ed uscite dal programma.
Se un distruttore chiama una qualunque funzione, quella funzione potrebbe lanciare un’eccezione. Un distruttore non può lanciare un’eccezione (ne potrebbe risultare una chiamata a terminate(), che indica un errore di programmazione), quindi qualunque distruttore che chiama funzioni deve catturarne e gestirne le eccezioni.
Non create una vostra notazione “personalizzata” per i nomi delle variabili membro (underscore prefissi, notazione ungherese e così via), a meno che non abbiate una gran quantità di variabili globali preesistenti; se così non è, lasciate che le classi ed i namespace svolgano il lavoro per voi.
Prestate attenzione all’overload. Una funzione non dovrebbe eseguire del codice condizionatamente, in base al valore di un argomento, che sia o meno di default. In questo caso, dovreste invece creare due o più funzioni in overload.
Nascondete i vostri puntatori all’interno di classi contenitore. Portateli all’esterno solo immediatamente prima di eseguire operazioni su di essi. I puntatori sono sempre stati una grossa fonte di errori. Quando utilizzate new, cercate di racchiuderne il risultato in un contenitore. Fate in modo che i contenitori “possiedano” i loro puntatori, in modo che siano responsabili per il rilascio delle risorse. Ancora meglio, inserite il puntatore in una classe; se volete ancora che si comporti come un puntatore, effettuate l’overload di operator-> e di operator*. Se dovete necessariamente avere un puntatore libero, inizializzatelo sempre, preferibilmente con l’indirizzo di un oggetto, ma se necessario anche a zero. Impostatelo a zero quando chiamate la delete, per prevenire eliminazioni multiple.
Evitate l’overload degli operatori new e delete a livello globale, ma fatelo sempre classe per classe. L’overload globale influenza sempre l’intero progetto, che invece andrebbe controllato solamente dal creatore del progetto. Al momento dell’overload di new e delete per una classe, non assumete di conoscere la dimensione dell’oggetto; qualcuno potrebbe stare ereditando da voi. Utilizzate l’argomento fornito. Se fate qualcosa di particolare, considerate l’effetto che potrebbe avere sugli eredi.
Prevenite lo sfaldamento degli oggetti. Non ha praticamente mai senso fare l’upcast di un oggetto per valore. Per prevenire l’upcast per valore, inserite funzioni virtuali pure nella vostra classe base.
Talvolta la semplice aggregazione è tutto ciò che serve. Un “sistema di confort per i passeggeri” di una linea aerea si compone di elementi sconnessi: sedile, aria condizionata, video, e così via; perdipiù avete bisogno di crearne molte istanze per un aereo. Forse che costruite dei membri privati e costruite da zero una nuova interfaccia? No - in questo caso i componenti appartengono anche all’interfaccia pubblica, quindi dovreste creare oggetti membro pubblici. Questi oggetti hanno una loro implementazione privata, che è ancora sicura. Tenete conto che l’aggregazione pura e semplice non è una soluzione da usare spesso, ma talvolta accade.