MEC++: Proxy

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quinto blocco: Tecniche.

(30) Proxy

A dire il vero, non ho capito bene il senso di questa tecnica. Me la rileggerò un'altra volta.

MEC++: Reference counting

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quinto blocco: Tecniche.

(29) Reference counting

Lo scopo del reference counting é permettere che differenti oggetti condividano la stessa rappresentazione interna di uno stesso valore. Ci sono due motivazioni fondamentali per usare questa tecnica: aiutare a gestire correttamente la costruzione e distruzione dell'oggetto sull'heap (in particolare, la distruzione va effettuata solo quando l'ultimo riferimento al valore viene a cadere), e l'esigenza di trattare come un unico oggetto diverse istanze che si riferiscano allo stesso valore, in modo da non allocare spazio inutile sullo heap.

MEC++: Puntatori vs riferimenti

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Primo blocco: Fondamentali.

(1) Puntatori e riferimenti

Puntatori e riferimenti (reference) appaiono diversi ma fanno cose molto molto simili. Le differenze fondamentali sono:

Un puntatore può assumere valore NULL ma non si può fare niente di simile con un riferimento.

Un riferimento deve riferirsi a un oggetto. Non é possibile creare un riferimento senza inizializzarlo.

Dato che un riferimento é per definizione associato ad un oggetto, non c'é la necessità di verificare che sia valido - per i puntatori é generalmente opportuno controllare che non sia NULL prima di utilizzarlo.

Un puntatore può puntare a differenti oggetti nel corso della sua vita, un riferimento é vincolato all'oggetto con cui viene inizializzato.

MEC++: Smart pointer

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quinto blocco: Tecniche.

(28) Smart pointer

Gli smart pointer sono oggetti che sembrano puntatori ma offrono funzionalità aggiuntive.

Usando smart pointer invece di puntatori sciocchi/grezzi (dumb/raw) si ottiene il controllo su questi aspetti del loro comportamento:
  • costruzione e distruzione: normalmente uno smart pointer viene inizializzato a zero; é possibili implementare tecniche per ridurre il rischio di memory e resource leak;
  • copia e assegnamento: a seconda dell'implementazione, si possono ottenere diversi effetti;
  • dereferenziamento: l'accesso all'oggetto sotteso può essere regolamentato a piacimento.
auto_ptr

Il problema di come comportarsi in caso di costruzione per copia e assegnamento per uno smart pointer é risolto da auto_ptr trasferendo la proprietà del puntatore dal vecchio al nuovo smart pointer.

Data questa impostazione, é una pessima idea passare un auto_ptr per valore. Vanno passati invece per const reference.

MEC++: Oggetti sull'heap

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quinto blocco: Tecniche.

(27) Vietare o imporre oggetti sullo heap.

Per impedire che un oggetto sia creato sullo stack basta dichiarare il distruttore privatamente. In questo modo sarà ancora possibile creare un oggetto sullo stack, ma quando il compilatore procederà alla sua rimozione automatica, troverà che la chiamata al distruttore gli é preclusa.

#include <iostream>
using namespace std;

class HeapBased {
private:
~HeapBased();
public:
void print() {
cout << "Hello." << endl;
}
};


int main() {
HeapBased hb;
hb.print();
}

Quando si compila il codice qui sopra, si ottengono errori come questi:

HeapBased.cpp:12: error: `HeapBased::~HeapBased()' is private
HeapBased.cpp:21: error: within this context

Per permettere la creazione di oggetti sullo heap aggiungiamo un metodo pubblico destroy() che richiama il distruttore privato:

#include <iostream>
using namespace std;

class HeapBased {
private:
~HeapBased();
public:
void destroy() const {
delete this;
}

void print() {
cout << "Hello." << endl;
}
};

HeapBased::~HeapBased() {
cout << "Object deleted" << endl;
}

int main() {
HeapBased* hb = new HeapBased();
hb->print();

hb->destroy();
}

Per proibire la generazione di oggetti sullo heap, d'altro canto, possiamo dichiarare come privato l'operator new, che viene richiamato automaticamente dal "new operator" globale. Per simmetria si dichiara privato anche operator delete, anche se la cosa sarebbe sovrabbondante.

MEC++: Limitare il numero di oggetti

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quinto blocco: Tecniche.

(26) Limitare il numero di oggetti per una data classe.

Per impedire di instaziare oggetti di una classe basta dichiarare privati, e non definire, i costruttori della classe:

class CantBeInstantiated {
private:
CantBeInstantiated();
CantBeInstantiated(const CantBeInstantiated&);

// ...

};


Per avere un solo oggetto, lo dichiariamo statico, all'interno della classe, e facciamo in modo che possa essere acceduto solo da una apposita funzione (statica), mantenendo privati i costruttori:

#include <iostream>
using namespace std;

class Printer {
public:
static Printer& getInstance();

void print() {
cout << "printing" << endl;
}

private:
Printer() {}
Printer(const Printer& rhs);

};

Printer& Printer::getInstance()
{
static Printer p;
return p;
}


int main() {
Printer& p = Printer::getInstance();
p.print();
}

Per gestire un numero limitato di oggetti, dobbiamo ricorre alle eccezioni. Lo schema prevede che il costruttore ne alzi una se il contatore che tiene traccia del numero di oggetti creati ha raggiunto il limite, altrimenti lo incrementa. Il distruttore decrementa il contatore.

Il rischio di questo schema é che gli oggetti potrebbero non essere distrutti correttamente.

MEC++: Ctor e funzioni non-membro virtuali

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quinto blocco: Tecniche.

(25) Costruttori e funzioni non-membro virtuali.

Un vero costruttore non può essere virtuale, non avrebbe senso. Ma é utile a volte avere una sorta di copy-ctor virtuale, che ci permetta di clonare un oggetto appartenente ad una gerarchia.

La cosa può essere fatta in questi termini:

class Base {
public:
virtual Base* clone() const = 0;
// ...
};

class Derived1 : public Base {
public:
virtual Derived1* clone() const
{ return new Derived1(*this); }
// ...
};

class Derived2 : public Base {
public:
virtual Derived2* clone() const
{ return new Derived2(*this); }
// ...
};


Altrettanto insensato é il concetto di funzione non-membro virtuale. Ma in alcuni contesti é altrettanto utile, tipo quando vogliamo ridefinire l'operatore << per una gerarchia di classi.

In questo caso la soluzione é semplice e lineare: definiamo una funzione non membro che ha il solo scopo di richiamare la funzione virtuale dell'oggetto.

Ad esempio:

class Base {
public:
virtual ostream& print(ostream& s) const = 0;
// ...
};

class Derived1 : public Base {
public:
virtual ostream& print(ostream& s) const;
// ...
};

class Derived2 : public Base {
public:
virtual ostream& print(ostream& s) const;
// ...
};

inline ostream& operator<<(ostream& s, const Base& c)
{
return c.print(s);
}

MEC++: I costi della virtual table

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(24) Funzioni virtuali, ereditarietà multipla, classi base virtuali, RTTI hanno un costo.

Quando viene chiamata una funzione virtuale, il codice eseguito deve corrispondere al tipo dinamico dell'oggetto su cui la funzione é invocata. In genere questo meccanismo viene implementato per mezzo di una virtual table e di puntatori alla virtual table, solitamente citati come vtbl e vptr.

La vtbl é solitamente un array di puntatori a funzione.

Tra i costi delle funzioni virtuali c'é dunque quello relativo alla allocazione della vtbl, che ha dimensioni proporzionali al numero di funzioni virtuali presenti nella classe.

Una classe che ha funzioni virtuali deve avere poi un membro nascosto che gli permetta di accederla, il vptr. In pratica la nostra classe occuperà più spazio in memoria rispetto alla sua versione non-virtuale.

Il costo di chiamare una funzione virtuale, rispetto a una non-virtuale é solo leggermente più alto, dato che tutto il meccanismo della determinazione del codice da invocare é fatto via calcoli su puntatori. Il problema é che é praticamente impossibile usare l'ottimizzazione "inline" su una funzione virtuale, dato che é una ottimizzazione a compile-time, mentre la risoluzione della chiamata a una funzione virtuale avviene a runtime.

L'ereditarietà multipla confonde ancora di più le acque, complicando la gestione della vtbl e richiedendo un vptr per classe base nella classe considerata.

Inoltre la MI porta spesso alla necessità di dichiarare la classe base come virtuale, implicando altri costi.

RTTI ci permette di avere informazioni su ogni classe e oggetto a runtime, che sono memorizzate in un oggetto di tipo type_info, accedibile per mezzo dell'operatore typeid. In genere la RTTI viene implentata basandosi sulla vtbl.

MEC++: librerie alternative

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(23) Può essere opportuno considerare librerie alternative.

Ad esempio, se le prestazioni sono una richiesta stringente, può essere vantaggioso usare stdio (con tutti i suoi limiti) invece di iostream.

MEC++: op= vs. op

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(22) op= può essere una scelta migliore di op.

Ad esempio, in una classe potrebbe essere utile avere, oltre ad "operator+" e "operator=", anche "operator +=".

L'effetto interessante é che += può essere più efficiente di +, eliminando la necessità di creare un oggetto temporaneo.

MEC++: Evitare la conversione di tipo implicita

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(21) Usare l'oveload per evitare la conversione di tipo implicita.

La conversione di tipo implicita é utilizzata quando il compilatore non trova una funzione per il tipo richiesto. Ergo, se vogliamo evitare questa conversione occorre fornire un overloading alla funzione che richieda il tipo di parametro che ci interessa.

MEC++: Ottimizzazione del valore di ritorno

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(20) Facilitare l'ottimizzazione del valore di ritorno.

Spesso il compilatore può ottimizzare il codice in modo da eliminare l'aggravio del costo di tornare un oggetto by-value. Per aiutare il compilatore in questa ottimizzazione si può scrivere il codice in questo modo:

const Rational operator*(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}

Si torna il costruttore di un oggetto. In genere il compilatore é capace di risolvere questa indicazione evitando la creazione di un oggetto temporaneo.

Meglio ancora se la funzione é dichiarata come inline.

MEC++: Oggetti temporanei

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(19) Da dove vengono gli oggetti temporanei.

Un caso comune é quello dell'oggetto temporaneo creato dal compilatore per convertire un parametro passato ad una funzione al tipo effetivamente richiesto. Ad esempio, una funzione richiede un oggetto di tipo string, noi passiamo un char*.

Se il parametro é specificato come "const string&", il compilatore silenziosamente genera l'oggetto corretto per noi. Ma se invece fosse un "string&" non lo potrebbe fare, dato che, non essendo costante, sarebbe possibile fare delle modifiche a questo oggetto che andrebbero però perse alla fine della funzione, quando l'oggetto temporaneo viene distrutto.

Sta a noi decidere se intendiamo pagare il costo della creazione di un oggetto temporaneo o no.

MEC++: Eager evaluation

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(18) Ammortizzare il costo della computazione attesa.

A volte é opportuno fare più del necessario. Ad esempio quando abbiamo la ragionevole aspettativa che quello che ci viene richiesto é solo il primo passo di una serie di operazioni.

É una tecnica comunemente utilizzata nell'accesso alla memoria di massa, o alla lettura di informazioni su database.

MEC++: Lazy evaluation

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(17) Lazy evaluation.

Il massimo dell'efficienza lo si ottiene quando non si effettua nessun calcolo. Ma dato che solitamente é necessario fare qualcosa, prima o poi, può essere una buona strategia rimandare il più in là possibile.

MEC++: La regola 80-20

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Quarto blocco: Efficienza.

(16) La regola dell'80-20.

Per quanto mi ricordo, é una regola che viene dalla ricerca operativa: l'ottanta percento di un magazzino é occupato dal venti percento degli articoli presenti a catalogo.

Ma é una regola che vale anche nello sviluppo software. Si dice che un programma spenda solitamente l'ottanta percento del suo tempo ad eseguire il venti percento del codice.

Da cui si deduce che, prima di effettuare ottimizzazioni sul codice, occorre individuare dove é meglio intervenire. Altrimenti si corre il rischio di perdere tempo per ottimizzare codice che verrà eseguito di rado.

MEC++: I costi di una eccezione

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Terzo blocco: Eccezioni.

(15) I costi di una eccezione

Gestire le eccezioni costa. Anche se non le si usano nel proprio codice, l'infrastruttura é disponibile e rende il programma un po' più pesante di quanto potrebbe essere senza.

L'uso delle eccezioni, poi, fa crescere di un 5-10% la dimensione del codice e la velocità di esecuzione diminuisce circa nella stessa misura.

Occorre quindi usare le eccezioni per quello che sono, una misura per gestire eventi eccezionali.

MEC++: Specifica di eccezione

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Terzo blocco: Eccezioni.

(14) La specifica di eccezione va usata con cautela

Il problema é che se una funzione genera una eccezione che non é specificata nella sua dichiarazione, a runtime viene invocata la funzione unexpected() che di default chiama terminate() che a sua volta di default chiama abort().

Ed é facile correre il rischio che una nostra funzione generi una eccezione che non ci aspettiamo.

Il fatto é che questa funzione:

extern void f1();

può generare qualunque eccezione - per compatibilità con il codice esistente. Diventa quindi molto difficile accertarci di quali eccezioni possano essere generate da una funzione non banale.

MEC++: Il catch di una eccezione

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Terzo blocco: Eccezioni.

(13) Il catch di un eccezione va fatto per reference

Non é sicuro usare un puntatore, dato che si deve riferire ad un oggetto globale o statico. Ma questo é un requisito troppo facile da disattendere. Inoltre sarebbe in contrasto con le convenzioni del linguaggio, dato che le quattro eccezioni standard (bad_alloc, bad_cast, bad_typeid, bad_exception) sono oggetti.

Usare il mecccanismo by-value porta ad aggravi prestazionali (due copie dell'eccezione, invece della usuale singola copia) e al problema dello slicing, se si dovesse fare il catch di una eccezione derivata da quella specificata nella clausola.

Resta il by-reference, che risulta essere il modo migliore.

MEC++: lanciare un'eccezione

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Terzo blocco: Eccezioni.

(12) Lanciare un'eccezione, passare parametri, chiamare una funzione virtuale

In un certo senso, la clausola catch ricorda la dichiarazione di una funzione. In realtà, a parte il fatto che in entrambi i casi c'é un passaggio di un parametro, le situazioni sono ben diverse.

In primo luogo nel caso di eccezione si passa dal contesto in cui l'eccezione é generata a quello in cui viene gestita, senza poi ritornare al chiamante.

Notiamo la differenza tra questi due modi di gestire un'eccezione e poi propagarla:

catch(Widget& w)
{
// ...

throw;
}

catch(Widget& w)
{
// ...

throw w;
}

Nel primo caso dopo aver trattato l'eccezione, questa viene propagata direttamente. Nel secondo caso, ne viene creata una copia. Dunque, in generale, si preferisce la prima alternativa, che risparmia il costo della copia dell'oggetto eccezione.

MEC++: eccezioni nel dtor

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Terzo blocco: Eccezioni.

(11) Evita che eccezioni escano dal distruttore

Se il codice di un nostro dtor genera un'eccezione, e questa non viene gestita, il programma termina in modo anomalo.

Conviene quindi proteggere il codice con un catch-all:

Session::~Session()
{
try {
logDestruction(this);
}
catch (...) { }
}

MEC++: ctor ed eccezioni

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Terzo blocco: Eccezioni.

(10) Resource leak nel costruttore

In C++ solo gli oggetti completamente costruiti possono essere distrutti. Dunque se si genera un'eccezione durante l'esecuzione di un costruttore, l'oggetto in corso di creazione non può essere distrutto.

Il problema, ancora una volta, lo danno i puntatori, e per risolverlo basta usare smart pointer.

MEC++: dtor ed eccezioni

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Terzo blocco: Eccezioni.

(9) L'uso dei distruttori per prevenire resource leak.

Invece di usare puntatori conviene usare smart pointer, come auto_ptr, fornito dalla STL, che permette di avere i vantaggi di un puntatore e la semplicità di gestione di un oggetto sullo stack.

Principalmente in caso di eccezioni non dobbiamo preoccuparci di distruggere il puntatore in tutti i possibili rami di esecuzione, dato che il distruttore viene chiamato automaticamente quando lo smart pointer esce di scope.

MEC++: new e delete

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Secondo blocco: Operatori.

(8) I diversi significati di new e delete

Quando si parla di "new operator" si intende l'operatore che non può essere sottoposto ad overloading e che viene utilizzato al momento della creazione di un oggetto sull'heap, come in questo caso:

string* ps = new string("Memory Management");

Il "new operator" alloca memoria per l'oggetto e chiama il costruttore per inizializzare l'oggetto nel blocco di memoria che é stato allocato.

L'"operator new" é l'operatore che viene chiamato (implicitamente da "new operator") per allocare memoria. Non é comune chiamare l'"operator new" direttamente ma nel caso, lo si chiama in questo modo:

void* rawMemory = operator new(sizeof(string));

In questo caso ritorna un blocco di memoria, della dimensione di un oggetto della classe string. Si occupa solo di allocare la memoria, non ha niente a che vedere con il costruttore dell'oggetto.

Il "placement new" é l'operatore che ci permette di invocare il costruttore per un oggetto dato un blocco di memoria precedentemente allocato. Lo si chiama in questo modo:

new (buffer) MyClass();

Dove buffer é un puntatore all'area di memoria precedentemente allocata.

La chiamata a delete permette di deallocare la memoria precedentemente riservata all'oggetto in questione, e di chiamare il distruttore su di esso.

Se si usa "operator new" per allocare memoria senza richiamare il ctor, occorrerà usare operator delete per deallocarla, senza chiamare il dtor.

Se si usa il "placement new" occorrerà chiamare delete sull'oggetto e poi, esplicitamente, il dtor.

Infine occorre ricordarsi che quando si allocano array con new [], occorre liberare la memoria con delete [].

MEC++: Niente overload per questi operatori

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Secondo blocco: Operatori.

(7) Gli operatori && (and logico) || (or logico) , (virgola) non vanno sottoposti a overload

Il motivo principale per questo divieto, per quanto riguarda gli operatori and e or logico, é che non é possibile al programmatore replicare il comportamento standard di questi operatori nell'overloading.

Lo stesso dicesi per l'operatore virgola, che finirebbe per comportarsi in modo inconsueto.

I suddetti operatori potrebbero essere sottoposti a overload ma si consiglia di non farlo, per altri operatori non é proprio possibile. Sono quelli per il casting (C++ style), new, delete, sizeof, typeid, punto, punto-star, l'operatore di risoluzione di scopo ::, e l'operatore ternario ?:, gli altri operatori possono essere ridefiniti.

MEC++: operatori di incremento e decremento

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Secondo blocco: Operatori.

(6) Differenza tra operatori prefissi e postfissi di incremento e decremento

Per convenzione, quando si dichiara un operatore di incremento o decremento, la versione postfissa é quella che accetta come parametro (non utilizzato) un intero. La versione prefissa non ha parametri in ingresso.

É da notare, inoltre, che l'operatore prefisso ritorna un riferimento al tipo sotteso, mentre il postfisso ritorna un oggetto costante. Il motivo di questo comportamento diverso é nel fatto che il prefisso incrementa il valore corrente dell'oggetto e ritorna una referenza allo stesso, il postfisso deve generare una copia dell'oggetto corrente, incrementare l'oggetto e ritornare la copia (che ha valore pari a prima dell'incremento).

MEC++: conversioni implicite

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Secondo blocco: Operatori.

(5) Limita le funzioni custom per la conversione

Ci sono due modi per convertire un tipo custom in un altro: per mezzo di una chiamata al costruttore, o per mezzo dell'operatore di conversione implicita di tipo.

Ad esempio, il costruttore di una classe Name che converte una stringa in un oggetto di tipo Name avrà una signature di questo tipo:

Name::Name(const string& s);

Un operatore di conversione implicita di tipo sarà dichiarato ad esempio in questo modo:

class Rational {
public:
// ...
operator double() const;
}

Il problema é che l'operatore di conversione implicita viene chiamato, per l'appunto, implicitamente dal compilatore. E questo può rendere oscuro il comportamento del codice.

Per questo motivo si tende a preferire la conversione esplicita a quella implicita - quindi una funzione asDouble() invece di un operator double().

Per evitare che il costruttore venga utilizzato implicitamente lo si può marcare con la parola chiave explicit.

MEC++: Quando definire il default ctor

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Primo blocco: Fondamentali.

(4) Se non c'é bisogno, non definire un default ctor

Se una classe manca del costruttore di default, si complica la creazione di array di quel tipo, dato che alla creazione dell'array viene implicitamente chiamato il default ctor per ogni elemento.

Ma si può superare il problema invocando esplicitamente il costruttore con un opportuno parametro per ogni elemento. Ma questo vale solo per array sullo stack.

Oppure si può pensare di usare un array di puntatori, delegando la loro inizializzazione ad un susseguente ciclo for. Questo approccio porta ad un piccolo aumento della memoria necessaria per la gestione dell'array e, soprattutto, crea la necessità di ricordarsi di distruggere tutti gli elementi esplicitamente, al termine del suo uso.

Si può evitare lo spreco di memoria usando la "placement new", a patto di rendere il codice meno leggibile. Soprattutto la distruzione degli oggetti risulta strana, dato che bisogna invocare esplicitamente il distruttore per ogni elemento, e farlo in ordine inverso rispetto alla costruzione.

Lo schema da seguire é questo:
  • si alloca la memoria per lo spazio richiesto (n * dimensione della classe);
  • si converte via static_cast il puntatore risultate ad un puntatore alla classe;
  • si chiama espilicitamente il costruttore per ogni oggetto via "placement new";


for(int i = 0; i < n; ++i)
new(&array[i] MyClass( ... );

Alla fine dell'uso dell'array:
si chiama il distruttore esplicitamente

for(int i = n; i >= 0; --i)
array[i].~MyClass();

si dealloca la memoria:

operator delete[](rawMemory);


Un secondo problema per una classe senza default ctor é che non può essere usata con svariati container STL.

Il terzo problema é per le classi virtuali alla base di una gerarchia. Se non hanno il costruttore di default, l'intera gerarchia risulta più difficile da gestire.

Nonostante tutti questi problemi, é molto meglio incorrere in queste seccature, che quelle derivanti dall'avere un default ctor che non ha senso.

MEC++: Array e polimorfismo

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Primo blocco: Fondamentali.

(3) Gli array non vanno trattati polimorficamente

Il fatto é che per muoversi in un array occorre sapere la dimensione dell'elemento. Trattare un array polimorficamente vuol dire rendere difficile, o impossibile, al compilatore il compito di determinare dove si trova ogni elemento dell'array.

Ovvero, in pratica, un array di elementi polimorfi é una bestia che il C++ non riesce a gestire correttamente.

MEC++: Il cast alla C++

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Primo blocco: Fondamentali.

(2) Meglio usare il cast alla C++

É ancora possibile usare il cast come definito in C:

(tipo) espressione

Ma é meglio non farlo. Anche solo per il fatto che é praticamente impossibile ritrovare nel codice dove si é fatto un cast, usando questa notazione.

In C++ sono definiti quattro nuovi operatori per il cast: static_cast, const_cast, dynamic_cast e reinterpret_cast.

Il cast più comunemente usato é static_cast che permette, ad esempio, di convertire un intero in un double.

Dove in C si faceva così:

int firstNumber, secondNumber;
// ...
double result = ((double)firstNumber)/secondNumber;

In C++ si preferisce scrivere:

double result = static_cast(firstNumber)/secondNumber;


Per richiede il bypass dell'indicazione di costanza per un oggetto si usa const_cast.

L'operatore dynamic_cast é usato per fare downcast sicuri in una gerarchia di classi. Se il cast fallisce si ha un NULL (nel caso di puntatori) o una eccezione (per le reference).

Usare il dynamic_cast in una situazione in cui non é coinvolta una gerarchia di classi conduce ad un errore.

Infine reinterpret_cast ci permette di fare tutto quello che vogliamo: é nostra responsabilità di programmatore che il risultato di questo cast sia sensato.

MEC++: Puntatori vs riferimenti

Appunti tratti dalla rilettura di More Effective C++ di Scott Meyers. Primo blocco: Fondamentali.

(1) Puntatori e riferimenti

Puntatori e riferimenti (reference) appaiono diversi ma fanno cose molto molto simili. Le differenze fondamentali sono:

Un puntatore può assumere valore NULL ma non si può fare niente di simile con un riferimento.

Un riferimento deve riferirsi a un oggetto. Non é possibile creare un riferimento senza inizializzarlo.

Dato che un riferimento é per definizione associato ad un oggetto, non c'é la necessità di verificare che sia valido - per i puntatori é generalmente opportuno controllare che non sia NULL prima di utilizzarlo.

Un puntatore può puntare a differenti oggetti nel corso della sua vita, un riferimento é vincolato all'oggetto con cui viene inizializzato.

EC++: Object-Oriented

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Settimo blocco: Miscellanea.

(45) Cosa fa il C++ di default. Per una classe vuota il C++ genera automaticamente un costruttore, un copy-ctor, il dtor, l'operatore assegnamento, l'operatore indirizzo (normale e const).
(46) Gli errori segnalati dal compilatore e dal linker sono meglio di quelli a runtime.
(47) Occorre accertarsi che gli oggetti statici non locali siano inizializzati prima di essere usati.
(48) Fai attenzione ai warning del compilatore
(49) Familiarizzati con la libreria standard del C++
(50) Migliora la tua comprensione del C++

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.

EC++: new/delete vs. new[]/delete[]

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

(5) Usa la stessa forma nei corrispondenti new e delete.

Le due forme degli operatori per la gestione della memoria sono relative alla gestione di un singolo oggetto o di un array (usando le parentesi quadre).

Quando si alloca un array di oggetti, usando la forma new[], occorre deallocarli usando la corrispondente delete[], altrimenti si causerà un comportamento indefinito del nostro codice.

Questo il modo corretto di allocare/deallocare memoria per un singolo oggetto e per un array di oggetti:

string* pString = new string;
string* strings = new string[100];

// ...

delete pString;
delete[] strings;

EC++: commenti

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Primo blocco: passare dal C al C++.

(4) Meglio usare i commenti in stile C++.

Lo svantaggio dei commenti in stile C (/* ... */) é che si ingarbugliano nel caso di annidamenti.

I commenti C++ (// ...) hanno il vantaggio di valere da quel punto fino al termine della riga.

Ci sono evidentemente eccezioni. Ad esempio commentare una define in questo modo:

#define LIGHT_SPEED 3e8 // m/sec (in a vacuum)

é una richiesta di problemi, dato che il preprocessore sostituisce tutto quello che segue LIGHT_SPEED nel codice sorgente, introducendo il commento in modo quasi certamente inaspettato.

EC++: new e delete

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Primo blocco: passare dal C al C++.

(3) Meglio usare new e delete invece di malloc e free.

Un problema é che malloc e free non sanno nulla di costruttori e distruttori, si occupano solo dell'allocazione e rilascio della memoria.

É evidentemente una pessima idea usare nello stesso codice entrambi i modelli per la gestione della memoria.

EC++: iostream

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Primo blocco: passare dal C al C++.

(2) L'uso di iostream é da prefersi rispetto a stdio.h

La gestione dello standard i/o del C é eccellente ma non é object-oriented. In particolare scanf e printf non sono type-safe e non sono estensibili.

Meglio usare cin, cout, cerr e gli operatori >> e <<.

EC++: const

Appunti tratti dalla rilettura di Effective C++ di Scott Meyers. Primo blocco: passare dal C al C++.

(1) const e inline sono da prefersi a #define

Il motivo é che #define é una direttiva al preprocessore, meglio invece é appoggiarsi sul compilatore, quando possibile, in modo da avere un miglior supporto nel debugging.

In pratica, invece di scrivere:

#define ASPECT_RATIO 1.653

É meglio scrivere:

const double ASPECT_RATIO = 1.653;

Ci sono alcune sottigliezze su questo punto.

La prima riguarda i puntatori. In questo caso const può essere usato per fare in modo che sia il puntatore sia il puntato siano costanti. Quindi se vogliamo che valgano entrambe le restrizioni dobbiamo usare const due volte:

const char * const authorName = "Scott Meyers";


La definizione di una costante all'interno di una classe é una faccenda un po' più complessa.

Per prima cosa la costante deve essere dichiarata come static:

class Something {
// ...
static const int index = 12;
};

E poi deve essere definita:

const int Something::index;

In alternativa, per costanti intere, é sempre possibile ripiegare sull'uso di un enumeratore:

class Something {
// ...
enum { INDEX = 5 };
};

Per quanto riguarda l'altro tipico uso della direttiva #define, la creazione di macro, il C++ mette a disposizione la possibilità di risolvere inline una funzione, eliminando il costo della chiamata ma mantenendo la sicurezza dei controlli forniti dal compilatore. Per ottenere l'effetto di avere un singolo blocco di codice valido per i diversi tipi di dato, abbiniamo la specifica inline al template, usando le possibilità offerte dalla programmazione generica in C++. Il risultato é che la nota macro del C per determinare il maggiore tra due valori:

#define max(a,b) ((a) > (b) ? (a) : (b))

Viene riscritta in C++ in questo modo:

template
inline const T& max(const T& a, const T& b)
{ return a > b ? a : b; }

Scott Meyers

Un autore che difficilmente manca nella libreria di chi si occupi di sviluppo software in C++ é Scott Meyers.

Per quanto mi riguarda sono l'orgoglioso possessore di una copia della prima edizione di Effective C++ (l'orgoglio é mitigato dal fatto che c'é una lunga serie di correzioni e postille che vanno applicate al testo per renderlo aggiornato), e una copia di More Effective C++ che ogni tanto rivedo.

Non ho ancora affrontato Effective STL, ma penso che prima o poi finirò per cedere al fascino di fare tris.

STL - find e find_if

Dal quinto capitolo di Designing Components with the C++ STL, di Ulrich Breymann. Algoritmi standard.

Sezione dedicata agli algoritmi che non modificano le sequenze su cui operano.

Ci sono due versioni dell'algoritmo di ricerca, a seconda se si voglia specificare o meno un predicato da applicare alla ricerca.

La find() cerca il primo elemento nella sequenza che sia uguale al parametro passato, la find_if() cerca il primo che soddisfi il predicato passato come parametro.

Ecco l'esempio che mostra l'uso di find_if():

#include<algorithm>
#include<vector>
#include<iostream>

using namespace std;

void display(int x) {
cout << x << ' ';
}

class odd {
public:

bool operator()(int x) { // odd argument yields true
return x % 2;
}
};

int main() {
vector<int> v(8);
for (size_t i = 0; i < v.size(); ++i)
v[i] = 2 * i; // all even
v[5] += 1; // an odd number

// display
for_each(v.begin(), v.end(), display);
cout << endl;

// search for odd number
vector<int>::iterator it = find_if(v.begin(), v.end(), odd());
if (it != v.end()) {
cout << "The first odd number (" << *it << ") was found at position "
<< (it - v.begin()) << "." << endl;
}
else
cout << "No odd number found." << endl;
}

STL - for_each

Dal quinto capitolo di Designing Components with the C++ STL, di Ulrich Breymann. Algoritmi standard.

Sezione dedicata agli algoritmi che non modificano le sequenze su cui operano.

L'algoritmo for_each() opera non modifica direttamente la sequenza su cui opera, ma la funzione, o il functor (function object), che accetta come parametro può farlo.

A seguire un esempio che illustra l'uso di for_each sia con una funzione che con un functor.

#include<algorithm>
#include<vector>
#include<iostream>

using namespace std;

static void display(int x) {
cout << x << ' ';
}

class Increment { // functor class
private:
int increment;

public:

Increment(int i = 1) : increment(i) {
}

void operator()(int& x) { // mutating operator
x += increment;
}
};

int main() {
vector<int> v(5); // vector of 5 zeros

for_each(v.begin(), v.end(), display); // 0 0 0 0 0
cout << endl;

// v is changed by the functor, not by for_each:
for_each(v.begin(), v.end(), Increment(2));


for_each(v.begin(), v.end(), display); // 2 2 2 2 2
cout << endl;
}

STL - map e multimap

Dal quarto capitolo di Designing Components with the C++ STL, di Ulrich Breymann. Parte dedicata ai container associativi ordinati.

Il container associativo map gestisce collezioni di dati acceduti per mezzo di una chiave, secondo la quale sono ordinati gli elementi.

Questo é un piccole esempio d'uso per map:

#include<map>
#include<string>
#include<iostream>

using namespace std;

typedef map<int, string> MyMap;
typedef MyMap::value_type MyValue;
typedef pair<map<int, string>::iterator, bool> InsRet;

int main() {
MyMap myMap;
InsRet ret = myMap.insert(MyValue(836361136, "Andrew"));
if(ret.second == true) {
cout << (*(ret.first)).second << " inserted." << endl;
}
ret = myMap.insert(MyValue(274635328, "Berni"));
if(ret.second == true) {
cout << (*(ret.first)).second << " inserted." << endl;
}
ret = myMap.insert(MyValue(260736622, "John"));
if(ret.second == true) {
cout << (*(ret.first)).second << " inserted." << endl;
}
ret = myMap.insert(MyValue(720002287, "Karen"));
if(ret.second == true) {
cout << (*(ret.first)).second << " inserted." << endl;
}
ret = myMap.insert(MyValue(138373498, "Thomas"));
if(ret.second == true) {
cout << (*(ret.first)).second << " inserted." << endl;
}
ret = myMap.insert(MyValue(135353630, "William"));
if(ret.second == true) {
cout << (*(ret.first)).second << " inserted." << endl;
}
ret = myMap.insert(MyValue(720002287, "Xaviera"));
if(ret.second == false) {
cout << "As expected, item not inserted: key duplicated" << endl;
}

myMap[420602587] = "Willie";
myMap[420602587] = "Willie Nillie";

cout << endl << "The map sorted by key:" << endl;

for(MyMap::iterator it = myMap.begin(); it != myMap.end(); ++it) {
cout << (*it).first << ':' << (*it).second << endl;
}

cout << endl << "Finding an element by key: ";
int key = 720002287;
MyMap::iterator it = myMap.find(key);
if(it != myMap.end()) {
cout << key << " " << (*it).second << endl;
}

cout << "Same, using array notation: ";
cout << key << " " << myMap[key] << endl;
}

La multimap permette di avere chiavi duplicate, questo porta a rendere impossibile implementare l'operatore []. Inoltre, in modo simile a quanto visto per la coppia di classi set/multiset, anche per multimap il metodo insert() ritorna un iteratore all'elemento appena inserito.

STL - set e multiset

Dal quarto capitolo di Designing Components with the C++ STL, di Ulrich Breymann. Parte dedicata ai container associativi ordinati.

Un set é una collezione di elementi distinti. Non ci possono dunque essere due elementi uguali in una collezione. Nella implementazione STL gli elementi sono ordinati anche se questo non sarebbe strettamente necessario, se non viene specificato un ordinatore, viene usato come default less<T>.

Un esempio d'uso di set:

#include<iostream>
#include<set>

using namespace std;

/**
* check the pair returned by an insert operation on set
*/
static void checkInsertResult(pair<set<int>::iterator, bool> res) {
cout << "Insertion ";
if(res.second == false)
cout << "not ";
cout << "performed";
if(res.second == true)
cout << " for " << *(res.first);
cout << endl;
}

int main() {
set<int> mySet;

// inserting 10 elements
for(int i = 0; i < 10; ++i)
checkInsertResult(mySet.insert(i));

// trying to insert an element twice does not succeed
checkInsertResult(mySet.insert(4));

// dump
set<int>::iterator it = mySet.begin();
for(it = mySet.begin(); it != mySet.end(); ++it)
cout << *it << ' ';
cout << endl;

// looking for an element that is not there
it = mySet.find(12);
if(it == mySet.end())
cout << "Can't find element 12 (as expected)" << endl;

// checking for an element in the set
if(mySet.count(4) == 1)
cout << "The element 4 is in the set" << endl;

// find and remove an element
it = mySet.find(4);
if(it != mySet.end()) {
mySet.erase(it);
cout << "The element 4 erased" << endl;
}

// erase an elment directly
if(mySet.erase(6) == 1)
cout << "The element 6 erased" << endl;
if(mySet.erase(6) == 0)
cout << "Can't erase element 6 (as expected)" << endl;

// dump
it = mySet.begin();
while(it != mySet.end())
cout << *it++ << ' ';
cout << endl;
}

Esiste anche la classe multiset, che permette l'esistenza di elementi ripetuti; l'interfaccia é praticamente identica a quella di set, con la differenza sostanziale del metodo insert() che ritorna, in caso di successo, l'iteratore che punta all'elemento inserito nel container.

Set ordinati e no

Da Head First Java O'Reilly, capitolo 16 che tratta collezioni e programmazione generica.

Abbiamo detto che gli elementi di un Set sono unici. Ma dobbiamo specificare cosa si intende essere unico per un elemento.

Due oggetti sono considerati uguali in Java se vale one.equals(two) e one e two hanno il medesimo codice hash, come ritornato dal metodo hashcode(). Perché Set consideri un oggetto come duplicato dobbiamo ridefinire hashCode() e equals() in modo che i due oggetti siano visti come identici.

Per vedere se due reference puntino allo stesso oggetto possiamo usare l'operatore ==.

Dunque, se vogliamo poter usare la classe Song che abbiamo visto in questo post in un Set dobbiamo ridefinire il modo in cui due oggetti di tipo Song sono riconosciuti come identici. Aggiungeremo perciò la ridefinizione per il metodo equals() e hashCode() in questo modo:

public class Song implements Comparable<Song> {

private String title;
private String artist;
private int rating;

// ...

@Override
public boolean equals(Object o) {
if(o instanceof Song) {
Song s = (Song) o;
return s.title.equals(title) && s.artist.equals(artist);
}
return false;
}

@Override
public int hashCode() {
return title.hashCode() + artist.hashCode();
}

// ...
}

Data questa classe Song, possiamo riscrivere il nostro JukeBox per fare in modo che accetti in input una lista di brani non unici ma la collezione risultante contenga solo elementi unici.

Dobbiamo di nuovo prendere la decisione se vogliamo che la nostra collezione sia ordinata o meno. Se non siamo interessati all'ordinamento ci conviene usare HashSet, altrimenti TreeSet ci permette di mantenere la nostra collezione ordinata, al costo di un leggero aggravio dei tempi d'uso.
Nell'esempio che segue usiamo entrambi gli approcci:

package Chap16;

import java.util.*;

public class JukeBoxC {
private HashSet<Song> hs = new HashSet<Song>();
private TreeSet<Song> ts = new TreeSet<Song>();

public JukeBoxC() {
getSongs();
System.out.println(hs);
System.out.println(ts);
}

private void getSongs() {
// fake implementation
String[] fakeList = {"Pink Moon/Nick Drake/5", "Somersault/Zero 7/4",
"Shiva Moon/Prem Joshua/5", "Circles/BT (Brian Wayne Transeau)/3",
"Deep Channel/Afro Celts/3", "Passenger/Headmix/4",
"Pink Moon/Nick Drake/5", "Listen/Tahiti 80/3", "Listen/Tahiti 80/3"};
for(String s : fakeList) {
System.out.println(s);
String[] tokens = s.split("/");

int rating = Integer.parseInt(tokens[2]);
Song aSong = new Song(tokens[0], tokens[1], rating);
if(hs.add(aSong) == false) {
System.out.println("Duplicated song discarded");
}
ts.add(aSong);
}
}

public static void main(String[] args) {
new JukeBoxC();
}
}

Collezioni in Java

Da Head First Java O'Reilly, capitolo 16 che tratta collezioni e programmazione generica.

I principali tipi di collezioni resi disponibili in Java sono:
  • List: quando l'accento é sulla posizioni nella collezione. É possibile avere elementi ripetuti.
  • Set: quando vogliamo che gli elementi inclusi siano unici.
  • Map: quando si una chiave di accesso (univoca) agli elementi (che possono essere duplicati) nella collezione.
Se guardiamo la struttura delle classi e interfacce, scopriamo una ambiguità. Gran parte delle collezioni implementano, direttamente o indirettamente l'interfaccia Collection. Alcune però implementano l'interfaccia Map che é indipendente dalla gerarchia basata su Collection.

Le principali interfacce e classi definite in questo contesto sono:

Collection
Set [implementata da HashSet e LinkedHashSet], List [implementata da ArrayList, LinkedList e Array]
SortedSet [implementata da TreeSet]

Map [implementata da HashMap, LinkedHashMap e Hashtable]
SortedMap [implementata da TreeMap]

Una ArrayList ordinata /2

Da Head First Java O'Reilly, capitolo 16 che tratta collezioni e programmazione generica.

L'uso del metodo statico sort() della classe Collections, che abbiamo visto nel precendente post, diventa un poco più complicato in un caso più realistico, in cui la collezione non usi un tipo standard (String, nel caso visto) ma un tipo custom.

Infatti la dichiarazione del metodo sort() é la seguente:

public static <T extends Comparable<? super T>> void sort(List<T> list)

Ne consegue che, se intendiamo usare questo metodo sulla nostra collezione, questa deve essere basata su un tipo che implementi l'interfaccia Comparable.

Creiamo quindi la classe Song che descrive le canzoni come vogliamo vengano gestite dalla nostra applicazione:

package Chap16;

public class Song implements Comparable<Song> {
private String title;
private String artist;
private int rating;

public void setArtist(String artist) {
this.artist = artist;
}

public String getArtist() {
return artist;
}

public Song(String title, String artist, int rating) {
this.title = title;
this.artist = artist;
this.rating = rating;
}

public int compareTo(Song s) {
int result = title.compareTo(s.title);
if(result == 0)
return artist.compareTo(s.artist);

return result;
}

@Override
public String toString() {
return title + " by " + artist;
}
}

L'interfaccia Comparable richiede che si implementi il medoto compareTo(), nel nostro caso si compara il titolo e, in caso che questo risulti identico, l'artista.

L'altro punto che resta scoperto é la possibilità di ordinare la collezione usando un criterio diverso da quello standard per la classe sottostante. Diciamo che nel nostro caso si voglia realizzare un ordinamento anche in base all'autore e non solo al titolo del brano.

Il metodo sort() di Collections prevede la possibilità di specificare un oggetto comparatore che definisce come comparare gli oggetti durante l'ordinamento usando questa versione di sort:

public static <T> void sort(List<T> list, Comparator<? super T> c)


Definiamo come classe interna ArtistCompare che ha proprio questo scopo:

package Chap16;

import java.util.*;

public class JukeBoxB {
private ArrayList<Song> songs = new ArrayList<Song>();

public JukeBoxB() {
getSongs();
System.out.println(songs);
Collections.sort(songs);
System.out.println(songs);
Collections.sort(songs, new ArtistCompare());
System.out.println(songs);
}

private void getSongs() {
// fake implementation
String[] fakeList = {"Pink Moon/Nick Drake/5", "Somersault/Zero 7/4",
"Shiva Moon/Prem Joshua/5", "Circles/BT (Brian Wayne Transeau)/3",
"Deep Channel/Afro Celts/3", "Passenger/Headmix/4",
"Listen/Tahiti 80/3"};
for(String s : fakeList) {
System.out.println(s);
String[] tokens = s.split("/");

int rating = Integer.parseInt(tokens[2]);
songs.add(new Song(tokens[0], tokens[1], rating));
}
}

private class ArtistCompare implements Comparator {

public int compare(Song s1, Song s2) {
return s1.getArtist().compareTo(s2.getArtist());
}
}

public static void main(String[] args) {
new JukeBoxB();
}
}

Una ArrayList ordinata

Da Head First Java O'Reilly, capitolo 16 che tratta collezioni e programmazione generica.

Una operazione tipica che viene richiesta sulle collezioni é l'ordinamento. La collezione che abbiamo utilizzato prevalentemente in Head First Java é ArrayList, un array esteso di elementi non ordinati. Consideriamo un caso in cui siamo richiesti di leggere una serie di stringhe (titoli di brani musicali) e di presentarli in ordine alfabetico.

Abbiamo fondamentalmente un paio di alternative: o utilizziamo una collezione diversa, che mantenga i propri elementi in ordine, o applichiamo un ordinamento agli elementi della collezione.

La seconda alternativa é preferibile se la nostra collezione é tutto sommato statica, con pochi inserimenti. Se questo é il caso, possiamo utilizzare il metodo statico sort() della classe Collections per riorganizzare gli elementi come richiesto.

Vediamo un esempio:

package Chap16;

import java.util.*;

public class JukeBox {
private ArrayList<String> songs = new ArrayList<String>();

public JukeBox() {
getSongs();
System.out.println(songs);
Collections.sort(songs);
System.out.println(songs);
}

public static void main(String[] args) {
new JukeBox();
}

private void getSongs() {
// fake implementation
String[] fakeList = {"Pink Moon/Nick Drake", "Somersault/Zero 7",
"Shiva Moon/Prem Joshua", "Circles/BT (Brian Wayne Transeau)",
"Deep Channel/Afro Celts", "Passenger/Headmix", "Listen/Tahiti 80"};
for(String s : fakeList) {
System.out.println(s);
String[] tokens = s.split("/");
songs.add(tokens[0]);
}
}
}

Il metodo getSongs() simula la gestione dell'input da una fonte esterna di una lista di titoli e autori divisi dal carattere /. Qui ci interessano solo i titoli, facciamo dunque uno split di ogni stringa e prendiamo solo la prima parte per metterla nella collezione di canzoni.

Dopo aver caricato i brani eseguiamo una sort e vediamo come effettivamente l'operazione funzioni.

Chat client /2

Da Head First Java O'Reilly, capitolo 15. Networking e threads.

Dopo aver visto un semplice chat server multithread e un client minimale che gli si connette, vediamo ora un client che implementa anche la ricezione di messaggi dal server.

A livello di GUI il cambiamento consiste nell'introduzione di un oggetto JTextArea destinato a raccogliere i messaggi mandati dal chat server. Il metodo addIncoming() si fa carico di inizializzare la JTextArea mettendola in un pannello scrollabile e quindi nel pannello principale.

Il metodo connect() che stabilisce la connessione con il chat server estrae dal socket l'input stream, che verrà utilizzato dal chat server per mandare i messaggi, crea a partire da esso un reader su input stream, e quindi un reader bufferizzato che mette a disposizione della classe nella variabile d'istanza reader.

Ultimo cambiamento é il nuovo thread che viene creato e fatto partire al termine del costruttore del chat client. La classe che viene passata al costruttore del thread (che deve implementare l'interfaccia Runnable) é definita come inner class del chat client, MessagesReader.

Il metodo run() del MessageReader resta appeso sullo stream di input del socket, in attesa di messaggi. Come un messaggio viene ricevuto lo si aggiunge alla text area della GUI.

package Chap15;

import java.awt.BorderLayout;
import java.awt.event.*;
import java.io.*;
import java.net.Socket;
import javax.swing.*;

public class ChatClient2 {

JTextField outgoing = new JTextField(20);
JTextArea incoming = new JTextArea(15, 25);
PrintWriter writer;
BufferedReader reader;

public ChatClient2() {
JFrame frame = new JFrame("Chat Client 2");
JPanel panel = new JPanel();
JButton btnSend = new JButton("Send");
btnSend.addActionListener(new ButtonListener());

addIncoming(panel);
panel.add(outgoing);
panel.add(btnSend);
frame.getContentPane().add(BorderLayout.CENTER, panel);

frame.setSize(400, 400);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

connect();

new Thread(new MessagesReader()).start();
}

private void connect() {
try {
Socket socket = new Socket("localhost", 4242);

InputStreamReader isr = new InputStreamReader(socket.getInputStream());
reader = new BufferedReader(isr);
writer = new PrintWriter(socket.getOutputStream());
System.out.println("Connected to chat server");
}
catch (Exception ex) {
ex.printStackTrace();
}
}

private void addIncoming(JPanel panel) {
incoming.setLineWrap(true);
incoming.setWrapStyleWord(true);
incoming.setEditable(false);

JScrollPane scroller = new JScrollPane(incoming);
scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);

panel.add(scroller);
}

private class ButtonListener implements ActionListener {

public void actionPerformed(ActionEvent e) {
String txt = outgoing.getText();
System.out.println("Sending message: " + txt);
try {
writer.println(txt);
writer.flush();
}
catch (Exception ex) {
ex.printStackTrace();
}

outgoing.setText("");
outgoing.requestFocus();
}
}

private class MessagesReader implements Runnable {

public void run() {
String message;
try {
while ((message = reader.readLine()) != null) {
System.out.println("Message read: " + message);
incoming.append(message + "\n");
}
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}

public static void main(String[] args) {
new ChatClient2();
}
}

Anche questo client non é molto robusto. In particolare lo si deve lanciare solo quando il server é in esecuzione.

Nel server ho preferito non implementare la classe di appoggio come interna alla classe principale del server, in quanto la interazione tra le due classi é debole. Nel caso del client, c'é una interazione più forte, il MessageReader usa due variabili di istanza del client, reader e incoming, e inoltre il suo unico metodo, run(), é compatto e non mi pare renda troppo complesso il codice della classe esterna. Mi pare perciò più semplice usare una inner class per implementare questa relazione.

Chat client /1

Da Head First Java O'Reilly, capitolo 15. Networking e threads

Nel post precedente abbiamo visto un semplice chat server multithread. Ora vediamo il lato client.

La prima versione é molto semplice, serve più che altro a testare il server. Ci permette infatti solo di creare un messaggio e mandarlo al server.

La classe ChatClient costruisce una applicazione GUI minimale, i punti salienti del codice che vediamo a seguire sono nel metodo connect(), che stabilisce la connessione con il server (che deve essere in esecuzione!), e nella classe interna ButtonListener che mette a disposizione il metodo actionPerformed() che reagisce alla pressione del bottone:

package Chap15;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintWriter;
import java.net.Socket;
import javax.swing.*;

public class ChatClient {
JTextField message = new JTextField(20);
PrintWriter writer;

public ChatClient() {
JFrame frame = new JFrame("Chat Client");
JPanel panel = new JPanel();
JButton btnSend = new JButton("Send");
btnSend.addActionListener(new ButtonListener());

panel.add(message);
panel.add(btnSend);
frame.getContentPane().add(BorderLayout.CENTER, panel);

frame.setSize(400, 400);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

connect();
}

private void connect() {
try {
Socket socket = new Socket("localhost", 4242);
writer = new PrintWriter(socket.getOutputStream());
System.out.println("Connected to chat server");
} catch(Exception ex) {
ex.printStackTrace();
}
}

private class ButtonListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
String txt = message.getText();
System.out.println("Sending message: " + txt);
try {
writer.println(txt);
writer.flush();
} catch(Exception ex) {
ex.printStackTrace();
}

message.setText("");
message.requestFocus();
}
}

public static void main(String[] args) {
new ChatClient();
}
}

Nel metodo connect() creiamo un socket che cerca di connettersi alla macchina locale (localhost) sulla porta 4242. Se la connessione riesce creiamo un oggetto PrintWriter a partire dallo stream di output del socket nella variabile di istanza writer.
Il metodo actionPerformed() della classe interna ButtonListener, reagisce alla pressione del bottone leggendo il testo dalla input line e scrivendolo nel writer.

Un semplice chat server

Da Head First Java O'Reilly, capitolo 15. Networking e threads

Estendiamo il nostro esempio di una applicazione client/server creando un semplice sistema di chat. Il passo in avanti é che usiamo il multithreading (wow).

In questo post vediamo la parte server del sistema. Si tratta di un server semplificato al massimo, quello che fa é creare un server socket sulla porta 4242 e restare appeso in attesa di connessione dal lato client. Ogni volta che un client si connette creiamo un oggetto ChatClientHandler e facciamo partire un nuovo thread che gestisce la connessione.

Nel libro il client handler é una inner class del server. A me le classi interne mi innervosiscono, mi pare rendano il codice meno leggibile. In questo caso, poi, la connessione tra le due classi non é particolarmente forte, ho preferito quindi utilizzare un meccanismo di call back: il server passa se stesso alla classe interna in modo che questa possa richiamare il suo metodo tellEveryone().

Il client handler implementa l'interfaccia Runnable, in quanto vogliamo eseguire i suoi oggetti in differenti thread. Il costruttore ha come parametri il ChatServer, in modo da poter fare la callback, e il socket che usiamo per la connessione al client. Dal socket estraiamo l'input stream e con esso, via InputStreamReader, costruiamo il BufferedReader che, variabile di istanza, utilizziamo per leggere le comunicazioni che ci arrivano dal client.

Il metodo run() resta appeso in lettura sul BufferedReader. Come arriva una stringa facciamo la callback al chat server invocando il metodo tellEveryone() che manda il messaggio a tutti i sottoscrittori attivi della chat.

Questo il codice:

package Chap15;

import java.io.*;
import java.net.Socket;

public class ChatClientHandler implements Runnable {

private ChatServer server;
private BufferedReader reader;

public ChatClientHandler(ChatServer server, Socket socket) {
this.server = server;
try {
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
reader = new BufferedReader(isr);
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}

public void run() {
String message;
try {
while ((message = reader.readLine()) != null) {
System.out.println("Got message: " + message);
server.tellEveryone(message);
}
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}

Il chat server implementa il main(), che semplicemente crea un oggetto ChatServer. Nel costruttore il loop infinito crea un nuovo thread per ogni nuovo cliente che si connette alla chat e aggiunge un oggetto PrintWriter, derivato dal output stream del socket di connessione al client, alla lista mantenuta come variabile di istanza nell'oggetto.

Il metodo tellEveryone() viene chiamato dal gestore dei client e fa un loop su tutti gli oggetti PrintWriter nella lista, su ognuno dei quali chiama la println() per notificare il messaggio arrivato.

package Chap15;

import java.io.*;
import java.net.*;
import java.util.*;

public class ChatServer {
private ArrayList<PrintWriter> streams = new ArrayList<PrintWriter>();

public ChatServer() {
try {
ServerSocket ss = new ServerSocket(4242);
System.out.println("Chat server started on port 4242");

while(true) {
Socket client = ss.accept();
streams.add(new PrintWriter(client.getOutputStream()));

Thread t = new Thread(new ChatClientHandler(this, client));
t.start();
}
}
catch(Exception ex) {
ex.printStackTrace();
}
}

/**
* package visibility, to be used by the client handler
*/
void tellEveryone(String message) {
Iterator<PrintWriter> it = streams.iterator();
while(it.hasNext()) {
try {
PrintWriter pw = it.next();
pw.println(message);
pw.flush();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}

public static void main(String[] args) {
new ChatServer();
}
}

Da notare che il codice é veramente minimale e richiede una cooperazione assidua da parte dell'utilizzatore. Occorre lanciare prima il server, poi i client; un client che non trova il server ad attenderlo sulla porta specificata, infatti, non sa che fare. Allo stesso modo, per terminare l'esecuzione conviene prima chiudere il server e poi i client, altrimenti al server verrà recapitata un'eccezione ogni volta che un client chiude, dato che la readLine() al reader nel client handler verrà notificata una eccezione SocketException dovuta al reset della connesione sottostante.

Multithreading e concorrenza in Java /2

Da Head First Java O'Reilly, capitolo 15. Networking e threads

Nel post precendente abbiamo visto una applicazione multithread con un problema di concorrenza su di una risorsa condivisa. Ora vediamo come fare in modo che la nostra gestione del conto corrente funzioni come atteso.

Dobbiamo regolamentare l'accesso alla risorsa, ovvero vogliamo creare una sorta di coda di processi in esecuzione sul metodo withdrawl(). Per far questo in Java basta marcare il metodo come synchronized. In pratica questo é l'unico cambiamento che ci serve per evitare lo spiacevole comportamento che abbiamo osservato:

private synchronized boolean withdrawl(int amount) {
if(account.getBalance() >= amount) {
// ...
return true;
}

// can't withdraw
System.out.println(Thread.currentThread().getName() + ": out of money");
return false;
}


A livello implementativo quello che Java fa é mettere a disposizione un semaforo su ogni oggetto. Prima di eseguire un metodo synchronized il thread corrente controlla il semaforo. Se un altro thread é in esecuzione nella zona protetta, il thread corrente si mette in attesa del suo turno sul semaforo. Ma, dato che il semaforo é sull'oggetto e non sul metodo, se i metodi sincronizzati di una classe sono più di uno, non é possibile eseguire due di questi metodi contemporaneamente in due diversi thread.