• Non ci sono risultati.

Laboratorio di Programmazione aa 2012/2013 Late Binding

N/A
N/A
Protected

Academic year: 2022

Condividi "Laboratorio di Programmazione aa 2012/2013 Late Binding"

Copied!
6
0
0

Testo completo

(1)

Laboratorio di Programmazione aa 2012/2013

Late Binding

Giorgio Grisetti

email:[email protected] May 13, 2013

In questa lezione introdurremo il concetto di late bindng, ovvero “collegamento ritardato”. Il late binding e’ un meccanismo che premette di “sovrascrivere” metodi di una classe base da parte di una classe derivata, e non e’ una prerogativa del linguaggio c++, ma si puo’ implementare anche in C (seppur con maggior fatica).

Iniziamo la lezione con un semplice problema. Si considerin la classe Container, vista nella lezione precedente. La classe Container e’ implementata come un array di puntatori ad oggetti della classe Containable. E’ quindi lecito mettere nella classe Container una serie di puntatori ad oggetti di classe derivate da Container.

Il seguente frammento di programma effettua l’inserimento di un po’ di oggetti eterogenei nel con- tainer.

class Containeble { public:

// il significato di questa dichiarazione // diverra’ chiaro alla fine della lezione virtual ~Containable();

};

class C1: public Containable { void print() {

printf("sono un C1");

} };

class C2: public Containable { void print() {

printf("sono un C2");

} };

class Container { public:

...

// aggiunge un elemento al container void addElement(Containable* c);

// ritorna l’elemento in posizione i Containable* getElementAt(int i);

(2)

// ritorna l’indice del vettore in cui e’

// memorizzato ’elemento c // -1 se non presente int getElementIndex(int i);

// ritorna il numero di elementi int numElements();

protected:

// array di puntatori ad oggetti della classe containable // o suoi derivati

Containable** _elements;

};

int main() { Container cont;

C1* c1=new C1();

C2* c2=new C2();

cont.addElement(c1);

cont.addElement(c2);

}

Si vuole implementare una funzione che, dato un container, stampi tutti gli oggetti contenuti, invocando l’apposita funzione print. Si noti che una volta che gli oggetti c1 e c2 vengono messi nel container, perdono la loro identita’, diventando “Containable”. Se consideriamo solamente in container, nulla sappiamo del tipo “reale” di questi oggetti, siano essi di tipo “C1” o di tipo “C2”.

Vedremo nel seguito diverse soluzioni a questo problema, e ne analizzeremo i pro ed i contro.

1 Soluzione 1: Aggiunta di un campo “tipo” alla classe Con- tainable

Come abbiamo detto, i nostri problemi sorgono dal fatto che non possiamo identificare il tipo “reale” di un oggetto, dato un puntatore ad una classe derivata. La soluzione piu’ semplice consite nell’aggiungere alla classe base una variabile che codifica il tipo, cosi’

class Containeble { public:

Containable() {_type = 0;}

int getType() {return _type;}

virtual ~Containable();

protected:

int _type; // variabile intera che rappresenta il tipo };

class C1: public Containable { C1() {

_type = 1;

}

void print() {

printf("sono un C1");

} };

class C2: public Containable { C2() {

(3)

_type = 2;

}

void print() {

printf("sono un C2");

} };

Abbiamo aggiunto una variabile _type alla classe container, che viene settata dai costruttori. Invocando il metodo getType() su un container otteniamo un numero che ci dice qual’e’ il tipo “reale” dell’oggetto in questione. In tal modo possiamo fare il cast al tipo corretto dove serve ed invocare il metodo print.

Per comodita’ scriviamo una funzione esterna alle classi che invoca il metodo print di un Containable void invokePrint(Containable* c) {

switch(c->getType(){

case 0: printf("error, the base class does not have print()"); break;

case 1: ((C1*)c)->print(); break;

case 1: ((C2*)c)->print(); break;

} }

La nostra funzione printContainable sara’ quindi:

void printContainable(Container& cont) { for (int i=0; i<cont.getNumElements(); i++)

invokePrint(cont.getElementAt(i));

}

La soluzione proposta risolve il problema, ma presenta un inconveniente. Ogni volta che aggiungiamo un nuovo tipo derivato da container, dobbiamo modificare la funzione “invokePrint”. In molti casi cio’

non e’ possibile poiche’ la funzione “invokePrint” puo’ essere contenuta in un file di libreria di cui non si dispone il sorgente. In C, la cosa viene risolta mediante l’uso di puntatori a funzione, come descritto nel paragrafo seguente.

2 Soluzione 2: Puntatori a funzione

Cos’e’ una funzione in un programma “compilato”? E’ niente altro che un insieme di istruzioni mem- orizzate in una regione (in genere contigua) di memoria. Cosa serve per invocare una funzione? Serve sapere le seguenti cose:

• quali argomenti passare alla funzione, ovvero la lista dei parametri.

• dove recupreare l’eventuale valore di ritorno.

• l’indirizzo in cui inizia la prima istruzione della funzione.

Nel momento in cui viene invocata, il nostro programma mettera’ nello stack i valori degli argomenti, e riservera’ spazio per contenere il valore di ritorno. Salvera’ quindi nello stack l’indirizzo dell’istruzione successiva a quella corrente (ovvero il pundo da cui riprendere l’esecuzione una volta che la funzione e’

terminata ed infine eseguira’ un “salto” alla prima istruzione della funzione. Quando la funzione termina, recuperera’ dallo stack l’indirizzo di ritorno e continuera’ da quel punto.

Si considerino le seguenti funzioni:

void function1(int j){

...

//do stuff }

void function2(int i){

(4)

...

//do cool stuff }

void function3(int i, char* v){

...

//do even cooler stuff }

si noti che dal punto do vista del compilatore invocazioni a function2 e function3 risultano in una struttura per i parametri sullo stack che e’ la stessa. Entrambe le funzioni, infatti ritornano un void e prendono un int come parametro. L’unica differenza quando si invoca function1 o function2 e’ l’indirizzo della prima istruzione della funzione.

Quindi, funzioni che hanno lo stesso tipo di valore di ritorno, e la stessa sequenza di tipi negli argomenti, differiscono nell’invocazione solamente per l’indirizzo della prima istruzione della funzione.

Detto in altre parole, se in un punti del programma compilato vogliamo sostituire un’invocazione di function1 con un’invocazione di function2 e’ sufficiente “scambiare” l’indirizzo di function1 con l’indirizzo di function2.

La cosa non vale se vogliamo invocare function3 al posto di function1, in quanto la lista dei parametri sara’ diversa e lo stack non avra’ la struttura richiesta da function3 per la sua esecuzione. Function3, infatti cerchera’ il parametro char* v, che non e’ sullo stack.

Ma se funzioni con la stessa lista di parametri e valore di ritorno sono intercambiabili e differiscono solo per l’istruzione di inizio, allora posso costruire un tipo “puntatore a funzione con lista di parametri come dico io”, ed assegnargli il valore dell’indirizzo di una funzione. In C un puntatore a funzione si dichiara cosi’:

<return type> (* <variable_name>) (<argument list>);

Con riferimento al listato precedente,

// declares a variable named myFancyPtr1, // compatible with function1 and function2 void (* myFancyPtr1)(int);

...

// assigns to function1 the pointer to function1 myFancyPtr = function1;

// invokes the function pointed by myFancyPtr (*myFancyPtr)(5); // calls function1(5);

// assigns to function1 the pointer to function2 myFancyPtr = function2;

(*myFancyPtr)(23); // calls function2(23);

// tries assigns to function3 the pointer to function2 // BUT FAILS because they are of different types myFancyPtr = function2; ///ERRORRRRRRRRRRRR

Con i puntatori a funzione possiamo trattare come dati dei comportamenti. Se all’interno di una classe stabiliamo che la variabile “printPtr” deve sempre puntare alla funzione di print di quella classe, e se tale variabile esiste nella classe base, possiamo risparmiarci la storia dell’identificatore di tipo. I puntatori a funzione possono solo contenere funzioni esterne alle classi o metodi statici (ovvero che non hanno oggetto di invocazione).

Con questi concetti, possiamo scrivere una nuova versione del programma di cui sopra, che fa uso dei puntatori a funzione;

class Containeble {

(5)

public:

Containable() {_printFn = 0;} // set the print fn to invalid;

virtual ~Containable();

protected:

void (*printFn)(); // functin pointer to the print function };

class C1: public Containable { C1() {

_printFn = C1::print; // function pointer to the print function of c1 }

static void print() { printf("sono un C1");

} };

class C2: public Containable { C2() {

_printFn = C1::print; // function pointer to the print function of c2 }

static void print() { printf("sono un C2");

} };

Si noti che nei costruttori degli oggetti viene sempre settato il valore della variabile printFn alla funzione di print opportuna. Dato un qualsiasi container,se vogliamo stamparlo e’ sufficiente invocare la funzione puntata dalla sua variabile d’istanza printFn. La nostra funzione printContainer diverra’

quindi:

void printContainable(Container& cont) { for (int i=0; i<cont.getNumElements(); i++){

Containable* c=cont.getElementAt(i);

if (c->printFn) // if the pointer is valid (*c->printFn)(); // call the function;

} }

Si noti che con questo meccanismo non dobbiamo mai piu’ modificare la funzione printContainable.

Ogni volta che aggiungiamo una nuova classe, per spiegare come stamparla e’ sufficiente scrivere una funzione opportuna, statica e nel costruttore della nuova classe dobbiamo assegnare alla variabile printFn il valore dell’indirizzo di inizio della funzione giusta.

Questo meccanismo e’ cio che avviene in pratica in una miriade di situazioni. I puntatori a funzione solo l’unico modo di invocare una funzione dal comportamento ignoto. Ogni volta che lanciate un programma, dopo che questo viene caricato in memoria c’e’ un salto ad una funzione (main). Ma il sistema operativo non sa che cosa il programma fa. Esegue semplicemente un salto ad una locazione di memoria fove il programma inizia. Dove e’ memorizzata tale locazione? In quale tipo di dato? Questo:

int (*mainFn)(int*, char**);

Pur essendo flessibili, i puntatori a funzione presentano un notevore overhead. E’ facile dimenticarsi di impostarli correttamente nei costruttori, inoltre esporre cose di basso livello come puntatori a memoria istruzioni e’ considerato “Rischioso”.

Il C++ offre un meccanismo per gestire tali costrutti in modo “elegante”, e senza dover fare nulla se non scrivere una parola chiave di fronte alla dichiarazione nel metodo nella classe base.

(6)

3 Soluzione 3: Metodi Virtual

Con la parola chiave “virtual” si dice al compilatore che una determinata funzione verra’ trattata come un puntatore, ma le assegnazioni e tutto il resto restano nascoste. Ad ogni tipo con almeno un metodo

”virtual”, e’ associata una “tabella dei metodi virtuali” che contiene I puntatori a tutte le funzioni dell’oggetto. Ogni oggetto con un metodo virtual ha un campo dati (nascosto), che e’ un puntatore alla tabella dei metodi. Esiste una tabella dei metodi virtuali per ogni tipo, non per ogni oggetto. Tutti gli oggetti dello stesso tipo condividono la stessa tabella.

Dichiarando un metodo virtual, si fa si che venga invocato quello del tipo “giusto”, e viene selezionato a tempo di esecuzione (non di compilazione), in quanto l’indirizzo della funzione viene letto da una variabile.

Con l’uso della parola chiave virtual il problema di cui sopra si risolve nel modo seguente:

class Containeble { public:

virtual ~Containable();

virtual void print(){} // dichiarazione vuota };

class C1: public Containable { void print() {

virtual printf("sono un C1");

} };

class C2: public Containable { void print() {

virtual printf("sono un C2");

} };

void printContainable(Container& cont) {

for (int i=0; i<cont.getNumElements(); i++) { Containable* c=cont.getElementAt(i);

c->print();

} }

Se in una classe si dichiarano uno piu’ metodi virtuali, il distruttore deve essere virtual.

4 Esercizio

Dato il frammento di codice precedente, mettere e togliere la parola virtual e vedere che succede.

Riferimenti

Documenti correlati

La passeggiata, passando da Piazza della Signoria e dal Palazzo degli Uffizi, si concluderà con uno dei panorami più suggestivi di Firenze: Ponte Vecchio, il ponte sull’Arno più

[r]

[r]

per le discipline che prevedono il voto scritto, minimo 2 prove scritte per quadrimestre. per le discipline che prevedono il voto orale, minimo 2 voti che potranno essere espressione

il Consiglio esprime parere favorevole alla partecipazione della classe ad attività inerenti alle tematiche scelte di Educazione Civica (Sviluppo sostenibile e Costituzione)

ANALISI SITUAZIONE DELLA CLASSE (classi 2) 2.1 PRESENTAZIONE DELLA CLASSE.. (considerare i seguenti indicatori: comportamento, rapporto con i docenti, capacità di ascolto,

Andò a Parigi dove c'era la torre Eiffel e… all'improvviso la corona toccò la torre Eiffel e diventò tutta rosa grazie alle sue stelline colorate e luminose.. La corona

L’immagine di insieme dell’edificio residenziale mostra i ponti termici sulle strutture portanti mentre analizzando le immagini di dettaglio sono apprezzabili altri difetti