[ 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 Marco Arena
Una delle caratteristiche importanti di C++ ereditata
dal C è l'efficienza. Se l'efficienza del C++ è drasticamente
minore del C, ci può essere una folta rappresentanza di programmatori
che non può giustificare il suo uso.
In C, uno dei modi di preservare l'efficienza è
attraverso l'uso di macro, queste permettono di fare ciò che
appare come una chiamata ad una funzione ma senza l'overhead tipico della
chiamata. La macro è implementata con il pre-processore invece che
dalla stesso processore, e il pre-processore sostituisce tutte le chiamate
alle macro direttamente con il codice delle macro, quindi non c'è costo
derivante dal pushing degli argomenti, esecuzione di una CALL del linguaggio
assembler, il ritorno di argomenti e l'esecuzione di un RETURN di linguaggio
assembler. Tutto il lavoro è eseguito dal pre-processore, così
si ha la convenienza e la leggibilità di una chiamata a funzione senza
costi aggiuntivi.
Ci sono due problemi con l'uso delle macro in C++.
Il primo già cruciale in C: una macro assomiglia a una chiamata di
funzione, ma non sempre agisce come tale. Questo può "nascondere"
errori difficili da trovare. Il secondo problema è specifico di C++:
il pre-processore non ha il permesso di accedere a dati delle classi membro.
Questo significa che le macro non possono essere usate come funzioni di classi
membro.
Per mantenere l'efficienza delle macro, ma aggiungere
la sicurezza e lo scope di classe di una vera funzione,il C++ offre le funzioni
inline. In questo capitolo, tratteremo dei problemi delle macro in C++,
di come questi siano stati risolti con le funzioni inline, delle linee guida
e delle intuizioni del modo di lavorare inline.
Il punto chiave dei problemi delle macro è che
ci si può ingannare nel pensare che il comportamento del pre-processore
è lo stesso del compilatore. Naturalmente è stato spiegato che
una macro è simile e lavora come una chiamata a funzione, per cui è
facile cadere in quest' equivoco. Le difficoltà iniziano quando si
affrontano le sottili differenze.
Come semplice esempio, si consideri il seguente:
#define F (x) (x + 1)
Ora, se viene fatta una chiamata F come questa
F(1)
il preprocessore la sviluppa, piuttosto inaspettatamente,
nella seguente:
(x) (x + 1)(1)
Il problema sorge a causa della distanza tra F
e la sua parentesi aperta nella definizione della macro. Quando questo spazio
viene rimosso, si può effettivamente chiamare la macro con lo
spazio
F (1)
ed essa sarà sviluppata correttamente in
(1 + 1)
L'esempio precedente è abbastanza insignificante
e il problema si renderà evidente subito. Le difficoltà reali
nasceranno quando saranno usate espressioni come argomenti nelle chiamate
a macro.
Ci sono due problemi. Il primo e che le espressioni
possono essere sviluppate all'interno delle macro per cui l'ordine di valutazione
è differente da ciò che normalmente ci si aspetta. Per esempio,
#define FLOOR(x,b) x>=b?0:1
Ora, se vengono usate espressioni per gli argomenti
if(FLOOR(a&0x0f,0x07)) // ...
la macro sarà sviluppata come
if(a&0x0f>=0x07?0:1)
La precedenza di & è minore rispetto
a quella di >=, quindi la valutazione della macro ci sorprenderà.
Una volta scoperto il problema, lo si può risolvere mettendo parentesi
intorno ad ogni cosa nella definizione della macro (questa è una buona
regola da usare quando si creano macro). Quindi,
#define FLOOR(x,b) ((x)>=(b)?0:1)
Scoprire il problema può essere difficile, comunque,
e si può non trovarlo finché non si è dato per scontato
l'esatto comportamento della macro. Nella versione senza parentesi della precedente
macro, molte espressioni lavoreranno correttamente perché la precedenza
di >= è minore di molti operatori come +, /, –,
e anche degli operatori di shift su bit. Quindi si può facilmente iniziare
a pensare che essa lavori con tutte le espressioni, incluse quelle che usano
gli operatori logici su bit.
Il problema precedente può essere risolto con
un'attenta pratica di programmazione: mettere le parentesi a tutto nelle macro.
Tuttavia, la seconda difficoltà è più insidiosa. Diversamente
da una funzione normale, ogni volta che si usa un argomento in una macro,
questo viene valutato. Finché la macro è chiamata solo con variabili
ordinarie, questa valutazione non avra effetti negativi, ma se la valutazione
di un argomento ha effetti collaterali, allora i risultati possono essere
sorprendenti e il comportamento non seguirà affatto quello di una funzione.
Per esempio, questa macro determina se il suo argomento
cade all'interno di un certo intervallo:
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
Finché si usa un argomento "ordinario", la macro lavorerà praticamente come una funzione reale. Ma non appena si diventa meno attenti pensando che sia una funzione reale, cominciano i problemi. Quindi:
//: C09:MacroSideEffects.cpp #include "../require.h" #include <fstream> using namespace std; #define BAND(x) (((x)>5 && (x)<10) ? (x) : 0) int main() { ofstream out("macro.out"); assure(out, "macro.out"); for(int i = 4; i < 11; i++) { int a = i; out << "a = " << a << endl << '\t'; out << "BAND(++a)=" << BAND(++a) << endl; out << "\t a = " << a << endl; } } ///:~
Da notare l'uso di caratteri maiuscoli per il nome della
macro. Questa è una regola utile perché dice al lettore che
essa è una macro e non una funzione, così se ci sono problemi
o dubbi ciò funziona da piccolo promemoria.
Ecco l'output prodotto dal programma, che non è niente affatto ciò che ci si sarebbe aspettato da una vera funzione:
a = 4 BAND(++a)=0 a = 5 a = 5 BAND(++a)=8 a = 8 a = 6 BAND(++a)=9 a = 9 a = 7 BAND(++a)=10 a = 10 a = 8 BAND(++a)=0 a = 10 a = 9 BAND(++a)=0 a = 11 a = 10 BAND(++a)=0 a = 12
Quando a vale quattro, solo la prima parte della
condizione basta (perché già non rispettata), quindi l'espressione
viene valutata una volta e l'effetto collaterale della chiamata a macro è
che a diventa cinque, il che è ciò che ci si aspetta
da una normale chiamata a funzione nella stessa situazione. Tuttavia, quando
il numero cade all'interno del range, verranno testate entrambe le condizioni
dell'if il che comporta due incrementi. Il risultato viene prodotto valutando
di nuovo l'argomento, per cui si passa a un terzo incremento. Una volta che
il numero esce fuori dal range, entrambe le condizioni sono ancora testate
avendo ancora due incrementi. Gli effetti collaterali sono differenti, dipendono
dall'argomento.
Questo non è chiaramente il tipo di comportamento
che si vuole da una macro che assomigli a una funzione. In questo caso, la
soluzione ovvia è renderla una vera funzione, il che naturalmente aggiunge
un overhead extra e può ridurre l'efficienza se si chiama spesso la
funzione. Sfortunatamente, il problema può non essere sempre così
ovvio, si può, ad esempio, avere una libreria che a nostra insaputa,
contenga funzioni e macro mischiate insieme, quindi un problema come questo
può nascondere bug molto difficili da trovare. Per esempio, la macro
putc( ) nella libreria cstdio può valutare il suo
secondo argomento due volte. Questo è specificato nello Standard C.
Inoltre anche le implementazioni poco attente di toupper( ) come
macro possono valutare l'argomento più di una volta, il che può
dare risultati inaspettati con toupper(*p++).[45]
Naturalmente, in C è richiesta una codifica accurata
e l'uso di macro, potremmo certamente ottenere la stessa cosa in C++ se non
ci fosse un problema: una macro non ha il concetto di scope richiesto
con le funzioni membro. Il preprocessore semplicemente esegue una sostituzione
di testo, quindi non si può dire qualcosa tipo
class X { int i; public: #define VAL(X::i) // Errore
o qualcosa simile. Inoltre , può non esserci
indicazione a quale oggetto ci si stia riferendo. Semplicemente non c'è
modo di esprimere lo scope di classe in una macro. Senza alcuna alternativa
per le macro, i programmatori sarebbero tentati di rendere alcuni dati public
per amore dell'efficienza, svelando così l'implementazione sottostante
e impedendo cambiamenti in quell'implementazione, così come l'eliminazione
della protezione fornita dalla direttiva private.
Risolvendo in C++ il problema delle macro con accesso
a classi membro di tipo private, verranno eliminati tutti i
problemi associati alle macro. Ciò è stato fatto portando il
concetto di macro sotto il controllo del compilatore dove esse appartengono.
Il C++ implementa le macro come funzioni inline, che è una vera
e propria funzione in ogni senso. Qualsiasi comportamento ci si aspetti da
una funzione ordinaria, lo si otterrà da una funzione inline. La sola
differenza e che una f.i. è sviluppata sul posto, come una macro, così
da eliminare l'overhead della chiamata a funzione. Pertanto, si possono (quasi)
mai usare le macro, ma solo le funzioni inline.
Ogni funzione definita all'interno del corpo di una
classe è automaticamente inline, ma è possibile rendere una
funzione inline facendo precedere la definizione di funzione dalla parola
riservata inline. Comunque, affinché si abbia un qualche effetto
si deve includere il corpo della funzione con la dichiarazione, altrimenti
il compilatore la tratterà come una funzione ordinaria. Pertanto,
inline int plusOne(int x);
non ha effetto su tutto ma solo sulla dichiarazione
della funzione (che in seguito può essere o meno una funzione inline).
L'approccio giusto prevede invece:
inline int plusOne(int x) { return ++x; }
Da notare che il compilatore verificherà (come
sempre) l'esattezza della lista degli argomenti della funzione e del valore
di ritorno (eseguendo eventuali conversioni), cose che il preprocessore è
incapace di fare. Inoltre provando a scrivere il codice precedente come una
macro, si otterrà un effetto indesiderato.
Quasi sempre le funzioni inline saranno definite in
un file header. Quando il compilatore vede una tale definizione, mette il
tipo di funzione (il nome insieme al valore di ritorno) e il corpo della funzione
stessa nella sua tabella dei simboli. Quando la funzione sarà richiamata,
il compilatore verificherà l'esattezza della chiamata e l'uso corretto
del valore di ritorno, e sostituisce il corpo della funzione con la chiamata
stessa, eliminando così l'overhead. Il codice inline occupa spazio,
ma se la funzione è piccola, ciò effettivamente prende meno
spazio del codice generato per fare una chiamata a una funzione ordinaria
(con il pushing degli argomenti nello stack e l'esecuzione della CALL).
Una funzione inline in un file header ha uno stato speciale,
si deve includere il file header contenente la funzione e la sua definizione
in ogni file dove la funzione è usata, ma non si finisce il discorso
con la definizione multipla di errori (comunque, la definizione deve essere
identica in tutti i posti dove la funzione inline è inclusa).
Per definire una funzione inline, si deve normalmente
precedere la definizione di funzione con la parola riservata inline.
Comunque ciò non è necessario all'interno delle definizioni
di classi. Ogni funzione definita all'interno di una classe è automaticamente
inline. Per esempio:
//: C09:Inline.cpp // L'inline all'interno delle classi #include <iostream> #include <string> using namespace std; class Point { int i, j, k; public: Point(): i(0), j(0), k(0) {} Point(int ii, int jj, int kk) : i(ii), j(jj), k(kk) {} void print(const string& msg = "") const { if(msg.size() != 0) cout << msg << endl; cout << "i = " << i << ", " << "j = " << j << ", " << "k = " << k << endl; } }; int main() { Point p, q(1,2,3); p.print("value of p"); q.print("value of q"); } ///:~
Qui, i due costruttori e la funzione print( )
sono tutte inline per default. Da notare che nel main( ) il fatto
che si stiano usando funzioni inline è del tutto trasparente, come
è giusto che sia. Il comportamento logico di una funzione deve essere
identico al di là del fatto che essa sia o meno inline (altrimenti
il compilatore è rotto). La sola differenza si noterà nelle
prestazioni.
Naturalmente, la tentazione è di usare le funzioni
inline ovunque all'interno delle dichiarazioni di classe perchè si
risparmia lo step extra di creare una definizione di funzione esterna. Ricordarsi
che la tecnica inline serve per fornire buone opportunità per l'ottimizzazione
del compilatore . Ma rendere inline una funzione grande
causerà una duplicazione di codice ovunque la funzione venga chiamata,
gonfiando il codice e diminuendo la velocità (il solo modo sicuro per
scoprirlo è sperimentare agli effetti di rendere inline un programma
sul proprio compilatore).
Uno degli usi più importanti della tecnica inline
all'interno delle classi è l'access function. Questa è
una piccola funzione che permette di leggere o cambiare parte dello stato
di un'oggetto - cioè variabili o una variabile interna. Il motivo per
cui la tecnica inline è importante per le access functions può
essere visto nel seguente esempio:
//: C09:Access.cpp // Inline e access functions class Access { int i; public: int read() const { return i; } void set(int ii) { i = ii; } }; int main() { Access A; A.set(100); int x = A.read(); } ///:~
Qui, l'utente della classe
non ha mai un contatto diretto con lo stato della variabile all'interno della
classe ed esse possono essere mantenute private, sotto il controllo
del progettista della classe. Tutti gli accessi a dati di tipo private
può essere controllato attraverso la funzione interfaccia. In più,
l'accesso è notevolmente efficiente. Si consideri il read( ),
per esempio. Senza l'inline, il codice generato per la chiamata a read( )
dovrà tipicamente includere il pushing (inserimento) nello stack e
fare una chiamata assembler CALL. Con molte macchine, la dimensione di questo
codice potrà essere più grande della dimensione del codice creato
dalla tecnica inline, e il tempo d''esecuzione potrà essere certamente
più lungo.
Senza le funzioni inline, un progettista che privilegia
l'efficienza sarà tentato di dichiarare semplicemente i di tipo
public, eliminando l'overhead permettendo all'utente di accedere direttamente
ad i. Da un punto di vista progettuale, ciò è disastroso
perchè i in tal caso diverrebbe parte dell'interfaccia pubblica,
il che significa che chi ha scritto la classe non può più cambiarla.
Si rimane bloccati con un int chiamato i. Questo è un
problema perchè ci si potrebbe accorgere prima o poi che può
essere più utile rappresentare quell'informazione come float
piuttosto che con un int, ma perchè int i è parte
di un interfaccia pubblica, non la si può più cambiare. Magari
si potrebbe voler eseguire calcoli supplementari oltre a leggere o settare
i, ma non si può se questo è public. Se,
d'altronde, si sono sempre usate funzioni per leggere o cambiare lo stato
di un'oggetto, si può modificare la rappresentazione sottostante dell'oggetto
(per la gioia del cuore..).
Inoltre, l'uso di funzioni per controllare dati permette
di aggiungere codice alla funzione per capire quando il valore del dato viene
cambiato, il che può essere molto utile durante il debugging. Se una
dato è public, chiunque può cambiarlo in qualunque momento.
Qualcuno preferisce dividere il concetto delle access
functions in accessors (leggere lo stato delle informazioni da un oggetto,
che accede) e mutators (cambiare lo stato di un oggetto, che
muta). Inoltre l'overloading delle funzioni può essere usato per
fornire lo stesso nome di funzione sia per l'accessor che per il mutator;
il modo in cui si chiama la funzione determina se si sta leggendo o modificando
lo stato dell'informazione. Ad esempio,
//: C09:Rectangle.cpp // Accessors e mutators class Rectangle { int wide, high; public: Rectangle(int w = 0, int h = 0) : wide(w), high(h) {} int width() const { return wide; } // Legge void width(int w) { wide = w; } // Setta int height() const { return high; } // Legge void height(int h) { high = h; } // Setta }; int main() { Rectangle r(19, 47); // Cambia width & height: r.height(2 * r.width()); r.width(2 * r.height()); } ///:~
Il costruttore usa la lista di inizializzazione (introdotta
nel capitolo 8 e trattata completamente nel capitolo 14) per inizializzare
i valori di wide e high (usando la forma dello pseudo costruttore
per i tipi predefiniti).
Non si possono avere nomi di funzioni e usare lo stesso
identificatore come dato membro, per cui si sarebbe tentati di distinguere
i dati con un trattino di sottolineatura. Comunque, gli identificatori con
il trattino di sottolineatura sono riservati e non si possono usare.
Si può scegliere invece di usare -get- e -set-
per indicare accessor e mutator:
//: C09:Rectangle2.cpp // Accessors e mutators con "get" e "set" class Rectangle { int width, height; public: Rectangle(int w = 0, int h = 0) : width(w), height(h) {} int getWidth() const { return width; } void setWidth(int w) { width = w; } int getHeight() const { return height; } void setHeight(int h) { height = h; } }; int main() { Rectangle r(19, 47); // Cambia width & height: r.setHeight(2 * r.getWidth()); r.setWidth(2 * r.getHeight()); } ///:~
Naturalmente, accessors e mutators non devono essere
semplici pipeline per una variabile interna. Qualche volta possono eseguire
calcoli più complicati. L'esempio seguente usa le funzioni della libreria
Standard C per produrre una semplice classeTime :
//: C09:Cpptime.h // Una semplice classe time #ifndef CPPTIME_H #define CPPTIME_H #include <ctime> #include <cstring> class Time { std::time_t t; std::tm local; char asciiRep[26]; unsigned char lflag, aflag; void updateLocal() { if(!lflag) { local = *std::localtime(&t); lflag++; } } void updateAscii() { if(!aflag) { updateLocal(); std::strcpy(asciiRep,std::asctime(&local)); aflag++; } } public: Time() { mark(); } void mark() { lflag = aflag = 0; std::time(&t); } const char* ascii() { updateAscii(); return asciiRep; } // Differenza in secondi: int delta(Time* dt) const { return int(std::difftime(t, dt->t)); } int daylightSavings() { updateLocal(); return local.tm_isdst; } int dayOfYear() { // Dal 1° Gennaio updateLocal(); return local.tm_yday; } int dayOfWeek() { // Da Domenica updateLocal(); return local.tm_wday; } int since1900() { // Anni dal 1900 updateLocal(); return local.tm_year; } int month() { // Da Gennaio updateLocal(); return local.tm_mon; } int dayOfMonth() { updateLocal(); return local.tm_mday; } int hour() { // Dalla mezzanotte, orario 24-ore updateLocal(); return local.tm_hour; } int minute() { updateLocal(); return local.tm_min; } int second() { updateLocal(); return local.tm_sec; } }; #endif // CPPTIME_H ///:~
Le funzioni della libreria Standard di C hanno diverse
rappresentazioni per il tempo, e queste fanno tutte parte della classe Time.
Comunque, non è necessario aggiornarle tutte, per tanto time_t t
è usata come rappresentazione base, tm local e asciiRep
(rappresentazione dei caratteri ASCII) hanno ciascuno dei flag per indicare
se devono essere aggiornate al valore corrente di time_t. Le due funzioni
private, updateLocal( ) e updateAscii( ) verificano
i flags e di conseguenza eseguono l'aggiornamento.
Il costruttore chiama la funzione mark( )
(che può essere anche chiamata dall'utente per forzare l'oggetto a
rappresentare il tempo corrente) e questo azzera i due flags per indicare
che l'ora locale e la rappresentazione ASCII non sono più valide. La
funzione ascii( ) chiama updateAscii( ), la quale
copia il risultato della funzione della libreria standard asctime( )
nel buffer locale perchè asctime( ) usa un'area dati statica
che viene sovrascritta ogni volta che si chiama. Il valore di ritorno della
funzione ascii( ) è l'indirizzo di questo buffer locale.
Tutte le funzioni che iniziano con daylightSavings( )
usano la funzione updateLocal( ), la quale provoca come conseguenza
per la struttura inline di essere abbastanza pesante. Questo non deve sembrare
utile, specialmente considerando che probabilmente non si vorrà chiamare
la funzione molte volte. Comunque, questo non deve significare che tutte le
funzione devono essere fatte non-inline. Se si vogliono tutte le altre funzioni
non-inline, conviene almeno mantenere updateLocal( ) inline, in
tal modo il suo codice sarà duplicato nelle funzioni non-inline, eliminando
l'overehead extra.
Ecco un piccolo programma test:
//: C09:Cpptime.cpp // Test di una semplice classe time #include "Cpptime.h" #include <iostream> using namespace std; int main() { Time start; for(int i = 1; i < 1000; i++) { cout << i << ' '; if(i%10 == 0) cout << endl; } Time end; cout << endl; cout << "start = " << start.ascii(); cout << "end = " << end.ascii(); cout << "delta = " << end.delta(&start); } ///:~
Un oggetto Time viene creato, poi vengono eseguite
alcune attività mangia-tempo e dopo viene creato un secondo oggetto
Time per segnare il tempo finale. Questi vengono usati per mostrare il
tempo iniziale, finale e trascorso.
Armati della tecnica inline, si possono adesso convertire
le classi Stash e Stack per una maggiore efficienza:
//: C09:Stash4.h // Inline functions #ifndef STASH4_H #define STASH4_H #include "../require.h" class Stash { int size; // Dimensione di ogni spazio int quantity; // Numero di spazi per lo storage int next; // Prossimo spazio libero // Array di bytes allocati dinamicamente: unsigned char* storage; void inflate(int increase); public: Stash(int sz) : size(sz), quantity(0), next(0), storage(0) {} Stash(int sz, int initQuantity) : size(sz), quantity(0), next(0), storage(0) { inflate(initQuantity); } Stash::~Stash() { if(storage != 0) delete []storage; } int add(void* element); void* fetch(int index) const { require(0 <= index, "Stash::fetch (-)index"); if(index >= next) return 0; // Per indicare la fine // Produce un puntatore all'elemento desiderato: return &(storage[index * size]); } int count() const { return next; } }; #endif // STASH4_H ///:~
Le funzioni piccole ovviamente lavorano bene con la tecnica inline, ma da notare che le due funzioni grandi sono ancora lasciate come non-inline, usando anche per loro la tecnica inline non si avrebbe un guadagno nelle performance:
//: C09:Stash4.cpp {O} #include "Stash4.h" #include <iostream> #include <cassert> using namespace std; const int increment = 100; int Stash::add(void* element) { if(next >= quantity) // Abbastanza spazio rimasto? inflate(increment); // Copia l'elemento nello storage, // parte dal prossimo spazio libero: int startBytes = next * size; unsigned char* e = (unsigned char*)element; for(int i = 0; i < size; i++) storage[startBytes + i] = e[i]; next++; return(next - 1); // Indice } void Stash::inflate(int increase) { assert(increase >= 0); if(increase == 0) return; int newQuantity = quantity + increase; int newBytes = newQuantity * size; int oldBytes = quantity * size; unsigned char* b = new unsigned char[newBytes]; for(int i = 0; i < oldBytes; i++) b[i] = storage[i]; // Copia il vecchio sul nuovo delete [](storage); // Rilascia lo storage vecchio storage = b; // Punta alla nuova memoria quantity = newQuantity; // Aggiusta la dimensione } ///:~
Ancora una volta, il programma verifica che tutto funzioni
correttamente:
//: C09:Stash4Test.cpp //{L} Stash4 #include "Stash4.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main() { Stash intStash(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout << "intStash.fetch(" << j << ") = " << *(int*)intStash.fetch(j) << endl; const int bufsize = 80; Stash stringStash(sizeof(char) * bufsize, 100); ifstream in("Stash4Test.cpp"); assure(in, "Stash4Test.cpp"); string line; while(getline(in, line)) stringStash.add((char*)line.c_str()); int k = 0; char* cp; while((cp = (char*)stringStash.fetch(k++))!=0) cout << "stringStash.fetch(" << k << ") = " << cp << endl; } ///:~
Questo è lo stesso programma-test usato prima,
per cui l'output deve essere sostanzialmente lo stesso.
La classe Stack fa perfino miglior uso della
tecnica inline:
//: C09:Stack4.h // Con l'inline #ifndef STACK4_H #define STACK4_H #include "../require.h" class Stack { struct Link { void* data; Link* next; Link(void* dat, Link* nxt): data(dat), next(nxt) {} }* head; public: Stack() : head(0) {} ~Stack() { require(head == 0, "Stack not empty"); } void push(void* dat) { head = new Link(dat, head); } void* peek() const { return head ? head->data : 0; } void* pop() { if(head == 0) return 0; void* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; } }; #endif // STACK4_H ///:~
Da notare che il distruttore Link, che era presente
ma vuoto nella precedente versione di Stack è stato rimosso.
In pop( ), l'espressione delete oldHead semplicemente libera
la memoria usata da Link (ciò non distrugge il dato puntato
daLink).
La maggior parte delle funzioni diventa inline piuttosto
esattamente e ovviamente, specialmente per Link. Perfino pop( )
sembra lecito, sebbene qualche volta si possono avere condizioni o variabili
locali per le quali non è chiaro che la tecnica inline sia la più
utile. Qui la funzione è piccola abbastanza tanto che probabilmente
non danneggia alcunché.
Se tutte le funzioni sono rese inline, l'uso della libreria
diventa abbastanza semplice perchè non c'e necessità del linking,
come si può vedere nell'esempio (da notare che non c'è Stack4.cpp):
//: C09:Stack4Test.cpp //{T} Stack4Test.cpp #include "Stack4.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main(int argc, char* argv[]) { requireArgs(argc, 1); // L'argomento è il nome del file ifstream in(argv[1]); assure(in, argv[1]); Stack textlines; string line; // Legge il file e memorizza le linee nello stack: while(getline(in, line)) textlines.push(new string(line)); // Estrae le linee dallo stack e le stampa: string* s; while((s = (string*)textlines.pop()) != 0) { cout << *s << endl; delete s; } } ///:~
C'è chi scriverà, in qualche caso, classi
con tutte funzioni inline, così che l'intera classe sarà in
un file header. Durante lo sviluppo di un programma ciò è probabilmente
innocuo, sebbene in qualche caso può rendere più lunga la compilazione.
Ma una volta che il programma si stabilizza, si può tornare indietro
e scrivere funzioni non-inline dove è possibile.
Per capire dove la tecnica inline è efficace,
è utile conoscere cosa fa il compilatore quando incontra una funzione
inline. Come per ogni funzione, il compilatore, memorizza il tipo della
funzione (cioè, il prototipo della funzione includendo il nome e i
tipi degli argomenti, insieme al valore di ritorno della funzione) nella sua
tabella dei simboli. In più, quando
il compilatore vede che il tipo della funzione inline e il corpo della funzione
sono analizzabili senza errori, anche il codice per la funzione viene tirato
dentro la tabella dei simboli. In
qualsiasi forma sia memorizzato il codice sorgente, istruzioni assembler compilate
o altre rapresentazioni spetta al compilatore deciderlo.
Quando si fa una chiamata ad una funzione inline, il
compilatore per prima cosa si assicura che la chiamata può essere fatta
in modo corretto. Ovvero tutti i tipi degli argomenti devono o essere giusti
nella lista degli argomenti o il compilatore deve essere in grado di fare
una conversione di tipo verso i tipi esatti ed inoltre il valore di ritorno
deve essere di tipo corretto (o convertibile) nell'espressione di destinazione.
Questo, naturalmente, è esattamente ciò che il compilatore fa
per ogni funzione ed è notevolmente diverso da ciò che fa il
preprocessore, che non può far verifiche sui tipi o eseguire conversioni.
Se tutte le informazioni sul tipo di funzione si adattano
con il contesto della chiamata, il codice inline viene sostituito direttamente
alla chiamata di funzione, eliminando l'overhead di chiamata e permettendo
ulteriori ottimizzazioni al compilatore. Inoltre, se il codice inline è
una funzione membro, l'indirizzo dell'oggetto (this) viene inserito
al posto giusto, il che, ovviamente, è un'altra azione che il preprocessore
non è in grado di fare.
Ci sono due situazioni nelle quali il compilatore non
può eseguire l'inlining. In questi casi, semplicemente, ritorna alla
forma ordinaria di una funzione prendendo la definizione inline e memorizzandola
proprio come una funzione non-inline. Se deve fare questo in unità
di conversioni multiple (le quali normalmente causano un errore di definizione
multipla), il linker è in grado di ignorare le definizioni multiple.
Il compilatore non può
eseguire l'inlining se la funzione è troppo complicata. Questo dipende
dal particolare compilatore, ma su questo punto molti compilatori rinunciano,
la tecnica inline quindi non apporterà probabilmente un aumento di
efficienza. In generale ogni sorta di loop è considerata troppo complicata
da espandere con la tecnica inline e, pensandoci sopra, un ciclo probabilmente
comporta molto più tempo all'interno della funzione che non quello
richiesto dall'overhead di chiamata. Se la funzione è solo un insieme
di semplici istruzioni, il compilatore non avrà probabilmente difficoltà
nell'applicare l'inlining, ma se ci sono molte istruzioni, l'overhead della
chiamata sarà minore del costo di esecuzione del corpo della funzione.
Si ricordi che ogni volta che si chiama una grossa funzione inline, l'intero
corpo della funzione viene inserito al posto della chiamata, per cui facilmente
si ottiene un "rigonfiamento" del codice senza apprezzabili miglioramenti
delle prestazioni (da notare che alcuni esempi di questo libro possono eccedere
le dimensioni ragionevoli per la tecnica inline in favore della salvaguardia
delle proprietà dello schermo).
Il compilatore inoltre non può eseguire l'inlining
se l'indirizzo della funzione è preso implicito o esplicito. Se il
compilatore deve produrre un indirizzo, allora esso allocherà memoria
per il codice della funzione e userà l'indirizzo derivato. Comunque,
quando un indirizzo non è richiesto, il compilatore probabilmente applicherà
la tecnica inline al codice.
E' importante capire che la tecnica inline è
solo una proposta al compilatore; quest'ultimo non è forzato a fare
niente inline. Un buon compilatore applicherà la tecnica inline con
funzioni piccole e semplici mentre intelligentemente ignorerà tale
tecnica per quelle troppo complicate. Ciò darà i risultati sperati:
l'esatta semantica della chiamata a funzione con l'efficienza di una macro.
Se si immagina cosa faccia il compilatore per implementare
la tecnica inline, ci si può confondere nel pensare che ci siano più
limitazioni di quante ne esistano effettivamente. In particolare, se una funzione
inline fa un riferimento in avanti ad un'altra funzione che non è stata
ancora dichiarata nella classe (al di là del fatto che sia inline o
meno), può sembrare che il compilatore non sia in grado di maneggiarlo:
//: C09:EvaluationOrder.cpp // Ordine di valutazione dell'inline class Forward { int i; public: Forward() : i(0) {} // Chiamata a funzioni non dichiarate: int f() const { return g() + 1; } int g() const { return i; } }; int main() { Forward frwd; frwd.f(); } ///:~
In f( ), viene fatta una chiamata a g( ), sebbene g( ) non è stata ancora dichiarata. Ciò funziona perchè il linguaggio stabilisce che le funzioni non-inline in una classe saranno valutate fino alla parentesi graffa di chiusura della dichiarazione di classe.
Naturalmente, se g( ) a sua volta chiama
f( ), si avrebbero una serie di chiamate ricorsive che sarebbero
troppo complicate per il compilatore da gestire con l'inline ( inoltre, si
dovrebbero eseguire alcune prove in f( ) or g( ) per
forzare una di esse a "raggiungere il livello più basso",
altrimenti la ricorsione sarebbe infinita).
Costruttori e distruttori sono due situazioni in cui
si può pensare che l'inline è più efficiente di quanto
non lo sia realmente. Costruttori e distruttori possono avere attività
nascoste, perchè la classe può contenere suboggetti dai quali
costruttori e distruttori devono essere chiamati. Questi suboggetti possono
essere normali oggetti o possono esistere a causa dell'ereditarietà
(trattata nel Capitolo 14). Come esempio di classe con oggetti membro:
//: C09:Hidden.cpp // Attività nascoste nell'inline #include <iostream> using namespace std; class Member { int i, j, k; public: Member(int x = 0) : i(x), j(x), k(x) {} ~Member() { cout << "~Member" << endl; } }; class WithMembers { Member q, r, s; // costruttori int i; public: WithMembers(int ii) : i(ii) {} // Insignificante? ~WithMembers() { cout << "~WithMembers" << endl; } }; int main() { WithMembers wm(1); } ///:~
Il costruttore per Member è abbastanza
semplice da trattare con la tecnica inline poichè non c'e niente di
speciale da fare - nessuna eredità o oggetti membro che
causano attività nascoste. Ma nella classe WithMembers c'e molto
di più di cui occuparsi di quanto salta all'occhio. I costruttori e
distruttori per gli oggetti q, r, e s sono stati chiamati
automaticamente, e quei costruttori e distruttori sono pure inline,
per cui la differenza da una normale funzione è significativa. Questo
non deve necessariamente significare che si dovrebbero sempre fare definizioni
di costruttori e distruttori non-inline; ci sono casi in cui ciò ha
senso. Inoltre, quando si sta facendo una "bozza" iniziale di un
programma per scrivere velocemente il codice, è spesso molto conveniente
usare la tecnica inline. Ma se si ricerca l'efficienza, è una situazione
da osservare .
In un libro come questo, la semplicità e concisione
di mettere le definizioni inline all'interno delle classi è molto utile
perché ben si adattano su una pagina o su un video (come in un seminario).
Comunque, Dan Saks[46]
ha posto in rilievo che in un progetto reale ciò ha l'effetto di ingombrare
inutilmente l'interfaccia della classe e quindi rendere la classe molto pesante
da usare. Egli ricorre a funzioni membro definite dentro le classi, usando
il latino in situ (sul posto), e sostiene che tutte le definizioni
dovrebbero essere piazzate fuori dalla classe per mantenere l'interfaccia
pulita. L'ottimizzazione che egli intende dimostrare è una questione
separata. Se si vuole ottimizzare il codice, si usi la parola riservata inline.Usando
questo approccio, l'esempio di prima, Rectangle.cpp diventa:
//: C09:Noinsitu.cpp // Rimuovere le funzioni in situ class Rectangle { int width, height; public: Rectangle(int w = 0, int h = 0); int getWidth() const; void setWidth(int w); int getHeight() const; void setHeight(int h); }; inline Rectangle::Rectangle(int w, int h) : width(w), height(h) {} inline int Rectangle::getWidth() const { return width; } inline void Rectangle::setWidth(int w) { width = w; } inline int Rectangle::getHeight() const { return height; } inline void Rectangle::setHeight(int h) { height = h; } int main() { Rectangle r(19, 47); // Transpone Width e Height: int iHeight = r.getHeight(); r.setHeight(r.getWidth()); r.setWidth(iHeight); } ///:~
Ora se si vuole paragonare l'effetto delle funzioni
inline con quelle non-inline, semplicemente si può rimuovere la keyword
inline (le funzioni inline dovrebbero trovarsi normalmente nei file
header, per quanto possibile, mentre le funzioni non-inline devono trovarsi
nella propria unità di conversione). Se si vogliono mettere le funzioni
nella documentazione, lo si può fare con una semplice operazione di
taglia-e-incolla. Le funzioni in situ necessitano di maggior lavoro
e potenzialmente possono presentare più errori. Un'altra controversia
per questo approccio è che si può sempre produrre uno stile
di formattazione coerente per le definizioni di funzioni, qualcosa che non
sempre è necessario con le funzioni in situ.
In precedenza, ho detto che quasi sempre si vogliono
usare le funzioni inline invece delle macro. Le eccezioni sorgono quando
si ha bisogno di usare tre caratteristiche speciali del preprocessore C (che
poi è anche il preprocessore C++): stringizing (convertire in stringhe
ndt), concatenazione di stringhe e token pasting (incollatura di identificatori).
Stringizing, introdotta in precedenza nel libro, viene eseguita con la direttiva
# e permette di prendere un identificatore e convertirlo in una stringa.
La concatenazione di stringhe si ha quando due stringhe adiacenti non hanno
caratteri di punteggiatura tra di loro, nel qual caso esse vengono unite.
Queste due caratteristiche sono particolarmente utili nello scrivere codice
di debug. Cioè,
#define DEBUG(x) cout << #x " = " << x << endl
Questo stampa il valore di ogni variabile. Si può anche ottenere una traccia che stampi le istruzioni eseguite:
#define TRACE(s) cerr << #s << endl; s
La direttiva #s converte
in stringa le istruzioni per l'output e la seconda s reitera
l'istruzione così esso viene eseguito. Naturalmente, questo tipo di
cosa può causare problemi, specialmente con un ciclo for di
una sola linea:
for(int i = 0; i < 100; i++) TRACE(f(i));
Poichè ci sono esattamente due istruzioni nella
macro TRACE( ), il ciclo for di una linea, esegue solo
la prima. La soluzione è di sostituire il punto e virgola con una virgola
nella macro.
Token pasting, implementato con la direttiva ##,
è molto utile quando si sta scrivendo il codice. Essa permette di prendere
due identificatori e incollarli insieme per creare automaticamente un nuovo
identificatore. Per esempio,
#define FIELD(a) char* a##_string; int a##_size class Record { FIELD(one); FIELD(two); FIELD(three); // ... };
Ogni chiamata alla macro FIELD( ) crea un
identificatore per memorizzare una stringa e un altro per memorizzare la lunghezza
di questa. Non solo è di facile lettura, ma può eliminare errori
di codice e rendere la manutenzione più facile.
Le funzioni require.h sono state usate fino a
questo punto senza definirle (sebbene assert( ) è già
stata usata per aiutare a trovare gli errori di programmazione dove appropriato).
E' il momento di definire questo file header. Le funzioni inline qui sono
convenienti perchè permettono ad ogni cosa di essere posizionata in
un file header, il che semplifica il processo di utilizzo dei package. Basta
includere il file header senza il bisogno di preoccuparsi del linking di file.
Si dovrebbe notare che le eccezioni (presentate in dettaglio nel Volume 2 di questo libro) forniscono un modo molto più efficace di maneggiare molte specie di errori – specialmente quelli che si vogliono riparare– invece di fermare il programma. Le condizioni che tratta require.h, comunque, sono quelle che impediscono la continuazione del programma, in modo simile a quando l'utente non fornisce sufficienti argomenti alla riga di comando o quando un file non può essere aperto. Perciò, è accettabile la chiamata alla funzione exit() della libreria Standard C.
Il seguente file header si trova nella root directory
del libro, per cui facilmente accessibile da tutti i capitoli.
//: :require.h // Test per le condizioni di errore nei programmi // Per i primi compilatori inserire "using namespace std" #ifndef REQUIRE_H #define REQUIRE_H #include <cstdio> #include <cstdlib> #include <fstream> #include <string> inline void require(bool requirement, const std::string& msg = "Richiesta fallita"){ using namespace std; if (!requirement) { fputs(msg.c_str(), stderr); fputs("\n", stderr); exit(1); } } inline void requireArgs(int argc, int args, const std::string& msg = "Devi usare %d argomenti") { using namespace std; if (argc != args + 1) { fprintf(stderr, msg.c_str(), args); fputs("\n", stderr); exit(1); } } inline void requireMinArgs(int argc, int minArgs, const std::string& msg = "Devi usare almeno %d argomenti") { using namespace std; if(argc < minArgs + 1) { fprintf(stderr, msg.c_str(), minArgs); fputs("\n", stderr); exit(1); } } inline void assure(std::ifstream& in, const std::string& filename = "") { using namespace std; if(!in) { fprintf(stderr, "Impossibile aprire il file %s\n", filename.c_str()); exit(1); } } inline void assure(std::ofstream& out, const std::string& filename = "") { using namespace std; if(!out) { fprintf(stderr, "Impossibile aprire il file %s\n", filename.c_str()); exit(1); } } #endif // REQUIRE_H ///:~
I valori di default forniscono messaggi ragionevoli
che possono essere cambiati se necessario.
Si noterà che invece di usare argomenti char*,
vengono usati const string&. Ciò permette per queste funzioni
argomenti sia char* che string, e quindi in generale molto più
utile (si può voler seguire questo modello nel proprio codice).
Nelle definizioni di requireArgs( ) e requireMinArgs( ),
viene aggiunto 1 al numero di argomenti necessari sulla linea di comando perchè
argc già include il nome del programma che viene eseguito come
argomento 0, e quindi già ha un valore che è uno in più
del numero degli argomenti presenti sulla linea di comando.
Si noti l'uso delle dichiarazioni locali “using
namespace std” dentro ogni funzione. Ciò perchè alcuni
compilatori nel momento della scrittura di questo libro non includevano erroneamente
le funzioni standard della libreria C in namespace std, per cui un
uso esplicito potrebbe causare un errore a compile-time.
La dichiarazione locale permette a require.h di lavorare sia con librerie
corrette che con quelle incomplete evitando la creazione di namespace std
per chiunque includa questo file header.
Ecco un semplice programma per testare require.h:
//: C09:ErrTest.cpp //{T} ErrTest.cpp // Test di require.h #include "../require.h" #include <fstream> using namespace std; int main(int argc, char* argv[]) { int i = 1; require(i, "value must be nonzero"); requireArgs(argc, 1); requireMinArgs(argc, 1); ifstream in(argv[1]); assure(in, argv[1]); // Use il nome del file ifstream nofile("nofile.xxx"); // Fallimento: //! assure(nofile); // L'argomento di default ofstream out("tmp.txt"); assure(out); } ///:~
Si potrebbe essere tentati di fare un passo ulteriore
per aprire file e aggiungere macro a require.h:
#define IFOPEN(VAR, NAME) \ ifstream VAR(NAME); \ assure(VAR, NAME);
Che potrebbero essere usate così:
IFOPEN(in, argv[1])
Dapprima, questo potrebbe sembrare interessante poichè
sembra ci sia da digitare di meno. Non è terribilmente insicuro, ma
è una strada che è meglio evitare. Si noti come, ancora una
volta, una macro appare come una funzione ma si comporta diversamente; essa
effettivamente crea un oggetto (in) il cui scope dura al di là
della la macro. Si può capire questo, ma per i nuovi programmatori
e i manutentori di codice è solo una cosa in più da decifrare.
Il C++ è già complicato abbastanza di per sè, per cui
è bene evitare di usare le macro ogni qualvolta si può.
E' di importanza cruciale essere abili a nascondere
l'implementazione sottostante di una classe perchè si può voler
cambiare questa in seguito. Si faranno questi cambiamenti per aumentare l'efficienza,
o perchè si arriva a una migliore comprensione del problema, o perchè
si rendono disponibili nuove classi che si vogliono usare nell'implementazione.
Qualsiasi cosa che metta in pericolo la privacy dell'implementazione riduce
la flessibilità del linguaggio. Per questo, la funzione inline è
molto importante perchè essa virtualmente elimina il bisogno delle
macro e i loro problemi correlati. Con la tecnica inline, le funzioni possono
essere efficienti come macro.
Le funzioni inline possono essere usate nelle definizioni
di classi, naturalmente. Il programmatore è tentato di fare così
perchè è più facile, e così avviene. Comunque,
non è che un punto di discussione, infatti più tardi, cercando
un'ottimizzazione delle dimensioni, si possono sempre cambiare le funzioni
in funzioni non-inline senza nessuno effetto sulla loro funzionalità.
La linea guida dello sviluppo del codice dovrebbe essere “Prima rendilo
funzionante, poi ottimizzalo.
Le soluzioni agli esercizi proposti può
essere trovata nel documento elettronico The Thinking in C++ Annotated
Solution Guide, disponibile a un costo accessibile, all'indirizzo www.BruceEckel.com.
[45]Andrew Koenig
entra in maggiori dettagli nel suo libro C Traps & Pitfalls (Addison-Wesley,
1989).
[46] Co-autore
con Tom Plum di C++ Programming Guidelines, Plum Hall, 1991.