MindView Inc.

 

[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[
Seminari ] [ Seminari su CD ROM ] [ Consulenza]

Pensare in C++, seconda ed. Volume 1

©2000 by Bruce Eckel

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

traduzione italiana e adattamento a cura di Marco Arena

A: Stile di codifica

Questa appendice non tratta dell'indentazione e posizionamento di parentesi tonde e graffe, sebbene ciò sarà menzionato.E' dedicato alle linee guida generali usate in questo libro per l'organizzazione dei listati del codice.

Sebbene molti di questi problemi sono stati introdotti in tutto il libro, questa appendice compare alla fine del libro in modo che si possa presumere che ogni argomento è aperto a critiche e se qualcosa non è chiaro ci si può rimandare alla sezione appropriata.

Tutte le decisioni riguardanti lo stile di codifica in questo libro sono state deliberatamente considerate e prese, qualche volta lungo un periodo di anni. Naturalmente, ognuno ha le proprie ragioni per organizzare il codice nel modo che vuole ed io proverò solo a dire come sono arrivato al mio considerando i vincoli e i fattori ambientali che mi hanno guidato a queste decisioni.

Nozioni generali

Nel testo di questo libro, gli identificatori (funzioni, variabili e nomi di classi) sono scritte in grassetto. Molte parole chiavi saranno scritte in grassetto, eccetto per quelle usate moltissimo per cui il grassetto può diventare tedioso, come "class" e "virtual".

Uso un particolare stile di codifica per gli esempi in questo libro. Questo è' stato sviluppato su un certo numero di anni ed è stato parzialmente ispirato dallo stile di Bjarne Stroustrup nel suo originale The C++ Programming Language.[64] Lo stile di formattazione è un buon tema per ore di dibattito, per cui dirò solo che non sto tentando di dettare il corretto stile con i miei esempi; ho le mie motivazioni per usare lo stile che uso. Poiché il C++ è un linguaggio di programmazione a stile libero, si può continuare ad usare qualsiasi stile con cui ci si trova comodi.

Detto questo, noterò che è importante avere uno stile di formattazione coerente all'interno di un progetto. Cercando in Internet, si troveranno un certo numero di tools che si possono usare per riformattare tutto il codice in un progetto per ottenere questa preziosa coerenza.

I programmi in questo libro sono file che vengono automaticamente estratti dal testo del libro, il che permette ad essi di essere testati per assicurarsi che funzionino correttamente. Quindi il codice stampato nel libro funzionerà senza errori di compilazione quando è compilato con un'implementazione conforme allo Standard C++ (da notare che non tutti i compilatori supportano tutte le caratteristiche del linguaggio). Gli errori che dovrebbero causare problemi di compilazione sono descritti con il commento //! per cui possono essere facilmente scoperti e testati usando gli strumenti automatici. Errori scoperti e segnalati all'autore compariranno prima nella versione elettronica del libro (su www.BruceEckel.com) ed in seguito aggiornati sul libro.

Uno degli standard in questo libro è che tutti i programmi saranno compilati e linkati senza errori (sebbene qualche volta causeranno dei warning). A questo fine, qualcuno dei programmi, che dimostra solo un esempio di codifica e non rappresenta un programma a se stante, avrà una funzione main( ) vuota, come questo

int main() {}

Questo permette al linker di finire senza errori.

Lo standard per main( ) è di restituire un int, ma lo Standard C++ stabilisce che se non c'è un istruzione di return all'interno di main( ), il compilatore genererà automaticamente il codice return 0. Questa opzione (niente return nel main( )) sarà usata in questo libro (alcuni compilatori potranno ancora generare dei warning per ciò, ma questi non sono compatibili con lo Standard C++).

Nomi di file

In C, è tradizione chiamare gli header file (contenenti le dichiarazioni) con l'estensione .h e i file di esecuzione ( che fanno sì che la memoria sia allocata e il codice generato) con l'estensione .c. Il C++ è passato attraverso un evoluzione. Fu prima sviluppato su Unix, dove il sistema operativo distingueva tra minuscole e maiuscole nei nomi di file. I nomi di file originali erano semplicemente la versione maiuscola dell'estensione C: .H e .C. Questo naturalmente non funziona per sistemi operativi che non distinguono minuscole e maiuscole, come il DOS. I venditori C++ sotto DOS usano estensioni come hxx e cxx per i file header e esecutivi, rispettivamente, hpp e cpp. Più tardi, qualcuno riuscì a capire che l'unica ragione per cui bisognava avere differenti estensioni per un file era perchè il compilatore potesse determinare se compilarlo come file C o C++. Poichè il compilatore non compilava mai gli header file direttamente, solo l'estensione dei file esecutivi doveva essere cambiata. La consuetudine, attraversando virtualmente tutti i sistemi, ha portato ad usare ora cpp per i file esecutivi e h per gli header file. Da notare che quando includiamo i file header standard di C++, si usa l'opzione di non avere estensione per i file, ad esempio: #include <iostream>.

Tag di inizio e fine commento

Un problema molto importante di questo libro è che tutto il codice che si vede in esso deve essere verificato per essere corretto ( con almeno un compilatore). Questo è realizzato estraendo automaticamente il file dal libro. Per facilitare ciò, tutti i listati che sono proposti per essere compilati ( al contrario dei frammenti di codice, dei quali ci sono solo alcuni) hanno tag di commento all'inizio e alla fine. Questi tag sono usati dal tool di estrazione del codice ExtractCode.cpp nel Volume 2 di questo libro ( che si può trovare sul sito www.BruceEckel.com) per estrarre ogni listato dalla versione ordinaria di testo ASCII del libro.

Il tag di fine listato dice ad ExtractCode.cpp che è alla fine del listato, ma il tag di inizio listato è seguito da informazioni quali la sotto-directory a cui il file appartiene (generalmente organizzate in capitoli, così un file appartenente al Capitolo 8 avrà un tag tipo C08), seguito da due punti ed il nome del listato.

Poichè ExtractCode.cpp crea anche un makefile per ogni sotto-directory, vengono anche incorporate nel listato informazioni su come un programma è fatto e la linea di comando usata per testarlo. Se un programma è a se stante ( non necessita di essere linkato con nessun altro cioè) non ha informazioni extra. Questo vale anche per gli header file. Comunque, se non contiene un main( ) e si intende che deve essere linkato con qualcos'altro, allora esso ha uno {O} dopo il nome del file. Se per il listato si intende che deve essere il programma principale ma necessita di essere linkato con altre componenti, c'è una linea separata che inizia con //{L} e continua con tutti i file che necessitano di essere linkati (senza estensioni, dal momento che questi possono variare da piattaforma a piattaforma).

Si possono trovare esempi in tutto il libro.

Se un file deve essere estratto ma i tag di inizio e fine non devono essere inclusi nel file che ne deriva (per esempio se esso è un file di dati di test) allora il tag di inizio è immediatamente seguito da un ‘!’.

Parentesi, graffe e indentazione

Si può notare che lo stile di formattazione in questo libro è differente da molti stili tradizionali di C. Naturalmente, ognuno pensa che il proprio stile sia il più razionale. Comunque, lo stile qui usato ha dietro una logica semplice, che sarà presentata qui insieme alle idee sul perchè sono stati sviluppati altri stili.

Lo stile di formattazione è motivato da una cosa: la presentazione, sia nella forma stampata che per i seminari. Si può avere la sensazione che i propri bisogni siano differenti perchè non si fanno molte presentazioni. Comunque, il codice operativo viene letto più volte di quanto è scritto e quindi deve essere facile per il lettore comprenderlo. I miei due importanti criteri sono la "scansione" ( come è facile per il lettore capire il significato di una singola linea) e il numero di linee che può adattarsi su una pagina. Quest'ultimo può suonare strano, ma quando si sta tenendo una presentazione in pubblico, si può far distrarre molto la platea se il presentatore deve spostarsi avanti e indietro tra le slide e ciò può essere causato da poche linee sparse.

Tutti sembrano concordare che il codice tra parentesi graffe dovrebbe essere indentato. Ciò su cui la gente non concorda - ed è il punto dove c'è maggior inconsistenza tra gli stili di formattazione - è questo: Dove vanno messe le parentesi aperte? Questa domanda, penso, è quella che causa tante variazioni tra stili di codifica (per un enumerazione di stili di codifica, vedi C++ Programming Guidelines, di Tom Plum e Dan Saks, Plum Hall 1991). Proverò a convincerti che molti degli stili odierni derivino dai vincoli del pre-Standard C (prima dei prototipi di funzione) e ora sono quindi inappropriati.

Prima, la mia risposta a quella domanda chiave: le parentesi graffe aperte dovrebbero sempre andare sulla stessa linea come il "precursore" ( per questo intendo " se il corpo è del tipo: una classe, funzione, definizioni di oggetti, istruzioni if, etc.."). Questa è una semplice, coerente regola che applico a tutto il codice che scrivo e rende la formattazione molto più semplice. Rende la "scansione" più semplice quando si guarda questa linea:

int func(int a);

si sa, dal punto e virgola alla fine della linea, che questa è una dichiarazione e non ha seguito, ma quando si vede la linea:

int func(int a) {

immediatamente si sa che è una definizione perchè la linea finisce con una parentesi aperta e non il punto e virgola. Usando questo approccio, non c'è differenza su dove mettere le parentesi di apertura per una definizione su più linee:

int func(int a) {
  int b = a + 1;
  return b * 2;
} 

e per una definizione su linea singola spesso usata per l'inline:

int func(int a) { return (a + 1) * 2; }

In modo simile per una classe:

class Thing;

è una dichiarazione di un nome di classe e

class Thing {

è una definizione di classe. Si può dire guardando la linea singola in tutti i casi se è una dichiarazione o una definizione. E naturalmente mettendo le parentesi aperte sulla stessa linea, invece che su una linea a parte, è possibile inserire più linee nella stessa pagina.

E quindi perchè abbiamo tanti altri stili? In particolare, si noterà che molte persone creano classi seguendo lo stile di sopra (il quale è usato da Stroustrup in tutte le edizioni del suo libro The C++ Programming Language ed. Addison-Wesley) ma creano definizioni di funzioni mettendo la parentesi aperta su una linea a sé (il che genera molti differenti stili di indentazione). Stroustrup fa questo eccetto che per brevi funzioni inline. Con l'approccio che descrivo qui, tutto è coerente - si può dire di cosa si tratta (class, funzione, enum, etc.) e sulla stessa linea si mette la parentesi aperta per indicare che il corpo per questa cosa è scritto a seguire. Inoltre, la parentesi aperta è la stessa sia per brevi funzioni inline che per le definizioni di funzioni ordinarie.

Sostengo che lo stile delle definizioni di funzioni usato da molte persone, derivi dal prototipo C pre-funzioni, nel quale non si dovevano dichiarare gli argomenti all'interno delle parentesi tonde, ma invece tra la parentesi tonda chiusa e la parentesi graffa aperte (questo mostra le radici assembly del linguaggio C):

void bar()
 int x;
 float y;
{
 /* il corpo qui */
} 

Qui, sarebbe completamente inopportuno mettere le parentesi graffe aperte sulla stessa linea, per cui nessuno lo fece. Comunque, si presero diverse decisioni in merito alla questione se le graffe dovrebbero essere indentate con il corpo o se esse dovrebbero essere al livello del "precursore". Per questo abbiamo differenti stili di formattazione.

Ci sono altri argomenti sul posizionamento delle graffe sulla linea immediatamente seguente la dichiarazione (di una classe, struct, funzione, etc.). La seguente viene da un lettore, ed è presentata in modo che si possa conoscere quale siano i problemi:

Utenti esperti di 'vi' sanno che schiacciando il tasto ']' due volte porterà alla prossima occorrenza di "{" (o ^L) in colonna 0. Questa caratteristica è estremamente utile per muoversi nel codice (per saltare alla prossima funzione o definizione di classe). [Il mio commento: quando stavo inizialmente lavorando sotto Unix, apparvero le GNU Emac e rimasi intrappolato tra esse. Come conseguenza, non ho mai raggiunto una piena conoscenza di vi e quindi non penso in termini di "locazione di colonna 0". Comunque, c'è un ampio gruppo di utenti di vi , ed essi sono colpiti da tale questione.]

Mettendo la "{" sulla linea successiva, si eliminano un po di confusione dal codice con istruzioni condizionali complesse, favorendo la "scansione". Esempio:

if(cond1
   && cond2
   && cond3) {
   istruzione;
} 

Il codice precedente [afferma il lettore] ha una scansione limitata. Comunque,

if (cond1
&& cond2
&& cond3)
{
istruzione;
} 

spezza l'if dal corpo, ma ha la conseguenza di una migliore leggibilità. [le nostre opinioni sul fatto che ciò sia vero varieranno in dipendenza da cosa siamo abituati a fare.]

Alla fine, è più facile allineare visualmente le graffe quando esse sono allineate nella stessa colonna. Esse visivamente "emergono" meglio. [fine del commento del lettore.]

Il problema di dove mettere le parentesi graffe aperte è probabilmente il più discordante. Ho imparato a scandire entrambe le forme e alla fine ne viene fuori quella con cui ci si trova più comodi. Comunque, noto che lo standard ufficiale Java ( trovato sul sito Java della Sun) è effettivamente lo stesso di quello che presento qui - da quando molte persone stanno iniziando a programmare in entrambi i linguaggi, la coerenza tra stili di codifica può essere utile.

L'approccio che uso rimuove tutte le eccezioni e casi speciali, e logicamente fornisce uno stile singolo di indentazione. Perfino all'interno del corpo di una funzione, la coerenza rimane, come in:

for(int i = 0; i < 100; i++) {
  cout << i << endl;
  cout << x * i << endl;
} 

Lo stile è semplice da insegnare e ricordare - si usa una singola, coerente regola per tutte le nostre formattazioni del codice, non una per le classi, due per le funzioni (una per quelle inline e un'altra quelle non), e possibilmente altre per cicli for, istruzioni if, ecc. La sola coerenza, penso, è meritevole di considerazione. Soprattutto, C++ è un linguaggio più nuovo che C, e sebbene dobbiamo fare molte concessioni al C, non dovremmo portare anche troppi manufatti con noi che causano problemi in futuro. Piccoli problemi moltiplicati per molte linee di codice diventano grandi problemi. Per un completo esame della materia, sebbene per il C, vedi C Style: Standards and Guidelines, di David Straker (Prentice-Hall 1992).

L'altro vincolo sotto il quale ho lavorato è la larghezza della linea, infatti il libro ha una limitazione a 50 caratteri. Cosa succede quando qualcosa è più lungo di una linea? Bene, di nuovo mi sono sforzato per avere una politica coerente per il modo in cui spezzare le linee, così che esse possano facilmente essere identificate. Finché qualcosa è parte di una singola definizione, lista di definizione, etc. la linea di continuazione dovrebbe essere indentata di un livello dentro dall'inizio della definizione, lista di argomenti, ecc.

Nomi degli identificatori

Coloro che hanno familiarità con Java noteranno che ho cambiato nell'usare lo stile standard di Java per tutti i nomi degli identificatori. Comunque, non posso essere completamente coerente qui perchè gli identificatori nelle librerie standard C e C++ non seguono questo stile.

Lo stile è abbastanza lineare. Solo la prima lettera di un identificatore è maiuscola se tale identificatore è una classe. Se si tratta di una funzione o variabile allora la prima lettera è minuscola. Il resto degli identificatori consistono di un o più parole, legate insieme ma distinte mettendo la maiuscola per ogni inizio parola. Così una classe appare in questo modo:

class FrenchVanilla : public IceCream {

Un identificatore di un oggetto appare così:

FrenchVanilla myIceCreamCone(3);

e una funzione invece:

void eatIceCreamCone();

(sia per una funzione membro che per una regolare funzione).

La sola eccezione riguarda le costanti compile-time (const o #define), per le quali tutte le lettere nell'identificatore sono maiuscole.

Il pregio dello stile è che scrivere con le maiuscole ha un suo significato - si può vedere dalla prima lettera se si sta parlando di una classe o di un oggetto/metodo. Ciò è particolarmente utile nell'accesso a classi membro di tipo static.

Ordine di inclusione dei file header

Gli header file sono inclusi in ordine "dal più specifico al più generale". Cioè ogni file header nella directory locale viene incluso per primo, poi ogni header del mio "tool" come require.h, successivamente vengono inclusi i file header di librerie di terzi, poi gli header della libreria Standard C++ e finalmente i file header della libreria C.

La giustificazione per questo viene da John Lakos nel libro Large-Scale C++ Software Design (Addison-Wesley, 1996):

Errori d'uso nascosti possono essere evitati assicurando che i file .h di una componente facciano l'analisi di se stessi - senza dichiarazioni o definizioni fornite dall'esterno.... Includendo i file .h nella prima linea del file .c ci si assicura che nessuno pezzo critico di informazione intrinseca all'interfaccia fisica del componente manchi dal file .h ( o, se c'è, ciò che si scoprirà su di esso non appena si proverà a compilare il file .c).

Se l'ordine di inclusione dei file header va "dal più specifico al più generale" allora è più verosimile che il proprio header non si analizzi da se, lo si scoprirà ben presto prevenendo fastidi lungo la strada.

Include guard nei file header

Gli include guard sono sempre usati all'interno dei file header per prevenire inclusioni multiple di un file header durante la compilazione di un singolo file .cpp. Le include guard sono implementate usando la direttiva del preprocessore #define controllando per vedere che un nome non sia già definito. Il nome usato per guard è basato sul nome del file header, con tutte le lettere del nome del file in maiuscolo e rimpiazzando il "." con un trattino di sottolineatura. Per esempio:

// IncludeGuard.h
#ifndef INCLUDEGUARD_H
#define INCLUDEGUARD_H
// Il corpo dell'header qui...
#endif // INCLUDEGUARD_H

L'indentificatore sull'ultima linea è incluso per chiarezza. Sebbene alcuni preprocessori ignorino ogni carattere dopo un #endif, ciò non è un comportamento standard per questo l'identificatore è commentato.

Uso dello spazio nei nomi

Nei file header, ogni violazione della spaziatura nei nomi nel quale è inclusa l'header deve essere scrupolosamente evitata. Cioè se si cambia la spaziatura fuori da una funzione o classe, si avrà che quel cambiamento si ritroverà per ogni file che include il nostro header, portando ad una serie di problemi. Nessuno dichiarazione using di qualsiasi tipo è permessa fuori dalla definizione di funzione, e nessuna direttiva using è permessa nei file header.

Nei file cpp, ogni direttiva using globale riguarderà solo quel file, per cui in questo libro esse sono generalmente usate per produrre codice più leggibile, specialmente in piccoli programmi.

Uso di require( ) e assure( )

Le funzioni require( ) e assure( ) definite in require.h sono usate in modo consistente in gran parte del libro, per cui potrebbero riportare errori. Se si ha familiarità con i concetti di precondizioni e postcondizioni (introdotti da Bertrand Meyer) si riconoscerà che l'uso di require( ) e assure( ) più o meno fornisce precondizioni (tipicamente) e postcondizioni (occasionalmente). Quindi all'inizio di una funzione, prima che parte del "nucleo" della funzione sia eseguito, vengono verificate le precondizioni per assicurarsi che ogni cosa sia giusta e che tutte le condizioni necessarie siano corrette. Poi il "nucleo" della funzione viene eseguito, e qualche volta vengono verificate alcune postcondizioni per essere sicuri che il nuovo stato dei dati rientri tra i parametri definiti. Si noterà che le verifiche delle postcondizioni sono are in questo libro, e assure( ) è principalmente usata per sincerarsi che i file siano aperti con successo.


[64] Ibid.

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
Ultimo Aggiornamento:24/12/2002