EC++: Object-Oriented

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Sesto blocco: Ereditarietà e progettazione Object-Oriented.

(35) La derivazione pubblica deve modellare un "is-a".
(36) Ereditare l'inerfaccia é diverso da ereditare l'implementazione.
(37) Le funzioni non virtuale ereditate non vanno ridefinite.
(38) Il valore di default di un parametro non deve essere ridefinito.
(39) Il downcasting non é una buona cosa. In caso non se ne possa proprio fare a meno, conviene almeno farlo in modo sicuro, usando dynamic_cast<>
(40) La relazione "has-a" o "é implementato in termini di" va modellata via composizione (layering, inclusione, ...).
(41) Ereditarietà vs. template. L'ereditarietà modella una gerarchia di classi, ognuna delle quali rappresenta un diverso comportamento, i template modellano una famiglia di classi che si distinguono per il tipo sottostante.
(42) L'ereditarietà privata va utilizzata con cautela. Quando possibile é meglio usare la composizione (layering).
(43) L'ereditarietà multipla (MI: Multiple Inheritance) va utilizzata con cautela.
(44) Occorre fare attenzione al fatto che quello che si dice corrisponda a quello che si vuole dire. Una classe comune vuol dire che le derivate hanno quei tratti in comune; l'ereditarietà pubblica indica una relazione "is-a"; l'ereditarietà privata indica una "implementazione in termini di"; composizione é per "has-a" o "implementazione in termini di".

EC++: implementazione di classi e funzioni

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Quinto blocco: Classi e funzioni: implementazione.

(29) Non tornare un accesso a dati interni, dato che l'oggetto potrebbe essere distrutto prima dell'ultimo utilizzo all'handle ai suoi dati interni.
(30) Non tornare un accesso via puntatori non-const a dati interni.
(31) Non tornare una referenza ad una variabile locale di una funzione.
(32) Ritarda il più possibile la definizione di una variabile.
(33) Non abusare di inline.
(34) Minimizza le dipendenze di compilazione tra file.

EC++: progettazione

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Quarto blocco: Classi e funzioni: progettazione e dichiarazione.

(18) L'interfaccia di una classe dovrebbe essere completa e minimale.
(19) Pro e contro di funzioni membro, non membro, friend.
(20) I membri data non dovrebbero apparire nella sezione public.
(21) Meglio usare const il più possibile.
(22) Meglio passare per reference al passare per valore.
(23) Quando occorre tornare un oggetto non si può tornare una reference.
(24) Overloading di una funzione vs. default per parametri.
(25) L'overload su puntatori e tipi numerici va evitato.
(26) Attenzione alle possibili ambiguità.
(27) Le funzioni membro generate implicitamente che non si vogliono usare vanno esplicitamente bloccate (dichiarandole private senza definirle).
(28) namespace va utilizzato per partizionare lo spazio dei nomi

EC++: cosa viene passato all'operator=

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Terzo blocco: Costruttori, distruttori e operatori di assegnamento.

(17) Controlla il caso di un assegnamento a sé stesso in operator=.

Il problema é quello dell'identità. Ovvero: quando possiamo dire che due oggetti sono in realtà lo stesso oggetto?

In genere é sufficiente controllare che l'indirizzo in memoria dei due oggetti sia il medesimo, ma a volte occorre effettuare controlli più approfonditi, tipicamente usando l'operatore == opportunamente ridefinito per una classe.

Il caso più comune é comunque questo:

C& C::operator=(const C& rhs)
{
if (this == &rhs)
return *this;

// ...
}

EC++: cosa fa operator=

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Terzo blocco: Costruttori, distruttori e operatori di assegnamento.

(16) In operator= vanno assegnati tutti i membri dati nella classe.

Se non si scrive esplicitamente l'operatore assegnamento per una classe, viene generato per noi. Purtroppo non é possibile lasciare che il C++ faccia il lavoro di default e occuparsi di gestire solo i membri che vogliamo vengano gestiti in modo diverso dal default. Se si scrive un operator=, questo si deve occupare dell'assegnamento di tutti i membri dato della classe.

Occorre prestare particolare attenzione a questa regola quando si modifica una classe aggiungendo un nuovo data-member e quando si crea una classe derivata.

Vediamo il caso di una semplice gerarchia di classi:

#include <iostream>
using namespace std;

class Base {
public:
Base(int x = 0) : _x(x) {
}
protected:
int _x;
};

class Derived : public Base {
public:
Derived(int x) : Base(x), _y(x) {
}

Derived & operator=(const Derived& rhs);

private:
int _y;
};

Date queste definizioni, sembrerebbe ragionevole definire l'operatore assegnamento per la classe Derived in questo modo:

Derived& Derived::operator=(const Derived& rhs) {
if (this == &rhs)
return *this;

_y = rhs._y;
return *this;
}

Ma sarebbe un errore, dato che questo porta ad aggiornare solo il dato membro relativo alla classe derivata, trascurando il membro della classe base. Occorre infatti esplicitamente chiamare l'operatore assegnamento per la classe base:

Derived& Derived::operator=(const Derived& rhs) {
if (this == &rhs)
return *this;

Base::operator=(rhs);
_y = rhs._y;
return *this;
}

La stessa cautela va tenuta nell'implementazione del copy-ctor. Se si scrive il costruttore-copia per la classe derivata, bisogna fare attenzione a chiamare il costruttore per la classe base, anche se questo non é definito esplicitamente:

Derived(const Derived& rhs) : Base(rhs), _y(rhs._y) {}

Altrimenti si cadrebbe nello stesso problema visto sopra, e il nuovo oggetto sarebbe inizializzato, nelle sue componenti relative alla classe di base, con i valori di default e non con la copia delle componenti dell'oggetto passato.

EC++: cosa torna operator=

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Terzo blocco: Costruttori, distruttori e operatori di assegnamento.

(15) L'operatore assegnamento deve tornare un riferimento a *this.

Per mantenere la simmetria tra l'operatore assegnamento come definito per i tipi standard, occorre che i tipi definiti dall'utente mantengano lo stesso comportamento.

Per una classe C ci si aspetta che venga perciò dichiarato così:

C& C::operator=(const C&);

É spesso utile avere overload per l'operatore assegnamento, ad esempio la classe string, può accettare anche un char* come parametro. Anche in questi casi, comunque il valore tornato deve essere un riferimento a *this:

string& operator=(const string& rhs);
string& operator=(const char *rhs);

EC++: virtual dtor per classi base

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Terzo blocco: Costruttori, distruttori e operatori di assegnamento.

(14) Il distruttore di una classe alla base di una gerarchia deve essere virtuale.

Il motivo di questo obbligo é che quando si distrugge l'istanza di una classe derivata per mezzo di un puntatore alla sua classe base e la classe base ha distruttore non-virtuale il risultato é, secondo lo standard del linguaggio, indefinito. Il che vuol dire, in genere, che viene chiamato solo il distruttore della classe base e non quello della derivata.

Per fare in modo che il comportamento sia quello atteso il distruttore della classe base deve essere dichiarato virtuale.

D'altra parte, se la classe non é pensata per essere alla base di una gerarchia di classi non é una buona idea dichiarare il suo distruttore come virtuale. Infatti dichiarare almeno una funzione di una classe come virtuale implica l'aggiunta di informazioni alla classe per permettergli di accedere alla virtual table - di solito questa informazione aggiuntiva é un puntatore.

A volte risulta utile dichiarare il distruttore come virtuale puro, anche se in realtà continua ad essere necessario definire il distruttore. Lo scopo di questa scelta é quello di rendere la classe astratta. Una classe é astratta, ovvero non può essere istanziata, se ha almeno una funzione virtuale pura. Ma se tutte le funzioni nell'interfaccia hanno una loro definizione di default, può essere utile definire come virtuale puro il distruttore (ricordandosi comunque di definirlo).

EC++: ordinamento nell'inizializzazione

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Terzo blocco: Costruttori, distruttori e operatori di assegnamento.

(13) I membri nella lista di inizializzazione devono essere nello stesso ordine in cui sono dichiarati.

Infatti i membri di una classe sono inizializzati nell'ordine in cui appare la loro dichiarazione nella classe. Usare un diverso ordinamento nel costruttore serve solo a rendere (possibilmente) oscuro il codice.

EC++: inizializzazione nel ctor

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Terzo blocco: Costruttori, distruttori e operatori di assegnamento.

(12) Nel costruttore si preferisce inizializzare ad assegnare.

Per assegnare i valori passati nel costruttore alle variabili membro si possono usare due approcci, l'inizializzazione e l'assegnamento:

// ...
int _x;
int* _px;

// initializing
A(int x, int* px) : _x(x), _px(px) {
}

// assigning
A(int x, int* px) {
_x = x;
_px = px;
}


Nel caso di variabili membro che siano const o referenze non c'é da scegliere: possono solo essere inizializzate.

Ma anche nel caso generale, come quello in esempio, si preferisce l'inizializzazione perché spesso più efficiente.

EC++: copy-ctor e operator=

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Terzo blocco: Costruttori, distruttori e operatori di assegnamento.

(11) Se la classe gestisce la memoria dinamicamente, é necessario dichiarare il costruttore copia e l'operatore di assegnamento.

Se non definiamo il copy-ctor e operator=, il C++ li genera per noi, con il risultato che la copia e l'assegnamento viene fatta bit a bit. Ovvero, nel caso vi siano membri puntatori, si copiano i puntatori e non i puntati. Con effetti che sono difficilmente quelli attesi.

Nel caso una classe non sia disegnata per permettere che le sue istanziazioni si copino e assegnino tra di loro, conviene dichiarare e non definire il copy-ctor e operator= nella sezione privata della classe, in modo da impedire queste funzionalità.

EC++: senza memoria

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Secondo blocco: Gestione della memoria.

(7) Attenzione alle condizioni di out of memory.

Se l'operatore new non può allocare memoria come richiestogli genera un'eccezione.

Però non é piacevole pensare di dover usare blocchi try-catch per tutte le parti del nostro codice dove si alloca memoria sull'heap per testare questa condizione.

Risulta più pratico usare una funzione nostra funzione set_new_handler() invece di quella standard.

Una soluzione brutale e non molto migliore di quella standard é questa:

void noMoreMemory() {
cerr << "Unable to satisfy request for memory\n";
exit(1);
}

int main() {
set_new_handler(noMoreMemory);

int *pBigDataArray = new int[400000000];
// ...

}

In realtà, dato un caso specifico, si può cercare di ottenere nuova memoria o attuare strategie particolari, prima di arrendersi e terminare l'esecuzione del programma.

EC++: delete nel dtor

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Secondo blocco: Gestione della memoria.

(6) Usa delete sui membri puntatori nel distruttore.

Per ogni puntatore che sia variabile membro di una classe occorre:
  • inizializzarlo in ogni costruttore, allocando memoria o assegnandogli 0;
  • nell'operatore assegnamento, chiamare delete su di esso e assegnargli il nuovo valore;
  • chiamare delete su di esso nel distruttore.


Al fine di eliminare tutte le seccature legate alla gestione dei puntatori, é una buona idea ricorrere agli smart pointer.