Newsgroup: it.comp.lang.c++
Titolo: Uso di "virtual".

Leggendo lo Stroustrup a proposito della keyword virtual...
/*
Cito da 12.2.6:
Le funzioni virtuali premettono di superare i problemi legati all'uso di un
campo tipo permettendo al programmatore di dichiarare funzioni in una classe
base che possono essere ridefinite in ogni classe derivata. Il compilatore e
il "loader" garantiranno la corretta corrispondenza tra oggetti e funzioni
ad esse applicate.
*/
Domanda: In ogni caso è possibile ridefinire una funzione avente lo stesso
nome in una classe derivata, mi chiedo quale sia la funzionalità delle
funzioni virtuali (a parte l'ovvia utilizzazione delle funzioni virtuali
pure...). L'ultima frase mi fa pensare che, per il fatto di definire
virtuale una funzione verrà effettuato un controllo sulla segnatura della
funzioni ridefinite nelle classi derivate. E' così?
Probabilmente è una sottigliezza che non riesco a cogliere, ma il libro in
questo punto mi è poco chiaro.
Cordialmente, Andrea.

 funzioni ridefinite nelle classi derivate. E' così?
Non ho capito.
--
Andrea Ferro
---------
Brainbench C++ Master. Scored higher than 97% of previous takers
Scores: Overall 4.46, Conceptual 5.0, Problem-Solving 5.0
More info 
http://www.brainbench.com/transcript.jsp?pid=2522556

 Non ho capito.
Se il problema è la segnatura di una funzione, io intendo l'ordine e il tipo
di parametri  della funzione (+ il parametro implicito). Volevo dire che
data una classe base che definisce un metodo virtual allora tutte le classi
derivate potevano ridefinire la stessa funzione ma senza la possibilità di
cambiare la segnatura della funzione.
Da quello che avevo capito la keyword virtual permetteva di forzare il
binding che di default è statico a dinamico nel caso in cui il metodo da
richiamare fosse risolubile solo a tempo di esecuzione.
Conferme, smentite??
Cordialmente, Andrea.


--


 Conferme, smentite??
Confermo. E' proprio cosi'.
Pero' il discorso della signature e' leggermente piu' complicato in quanto la
signature dell'override e' in relatà diversa da quella della funzione base. In
pratica a runtime viene chiamata una funzione con una signature laddove a
compile time c'era una chiamata ad una funzione con un'altra signature.
</Disclaimer ON>
Parlo di funzioni ma dovrei parlare di metodi. E parlo di signature intendendo
che una signature in se non puo' comprendere il concetto di virtual anche se
tecnicamente quello che avviene e' che virtual fa parte della signature.
</Disclaimer OFF>
Facciamo un esempio:
struct B {
    void foo();
    void foo2();
    virtual void bar();
};
struct B2 { int i; }
struct D: public B2, public B {
    void foo();
    void bar();
};
void test( D*d, B*b );
...
    D obj;
    test( &obj, &obj );
...
Questo e' il setup. Ora supponiamo di essere nel codice della funzione test.
Abbiamo due puntatori, uno a B e uno a D. Di fatto puntano entrambe allo stesso
oggetto concreto in termini logici, ma sono diversi (come evidenziato dall'uso
dell'eriditarietà multipla). Supponiamo di chiamare le varie funzioni foo.
b->foo();  // chiama B::foo()  ovvero "PAR b | CALL B::foo"
b->foo2(); // chiama B::foo2() ovvero "PAR b | CALL B::foo2"
d->foo();  // chiama D::foo()  ovvero "PAR d | CALL D::foo"
d->foo2(); // chiama B::foo2() ovvero "PAR CONVERT<B*> d | CALL B::foo2"
nota che nell'ultima chiamata avviene una conversione per il binding di this.
Ci si puo' chiedere come fare a chiamare B::foo() dal puntatore d. La risposta
e':
d->B::foo();  // chiama B::foo()  ovvero "PAR CONVERT<B*> d | CALL B::foo"
Fin'ora tutto semplice.
Usiamo ora la sintassi esplicita per chiamare le funzioni virtuali bar
bypassando il meccanismo del "virtual":
b->B::bar(); // chiama B::bar() ovvero "PAR b | CALL B::bar"
d->D::bar(); // chiama D::bar() ovvero "PAR d | CALL D::bar"
d->B::bar(); // chiama B::bar() ovvero "PAR CONVERT<B*> d | CALL B::bar"
Fino a questo punto ancora tutto bene. La domanda e' cosa avviene se chiamiamo
la virtuale normalmente.
Sappiamo che sia con b->bar() che con d->bar() verra' chiamata la stessa
funzione. Ovvero D::bar(). Pero' in entrambe i casi verrà chiamata con il
parametro this correttamente convertito in un D*. Quindi non e' corretto dire
che D::bar ha come signature un this di tipo B*. Il suo this e' sempre e
comunque D*, solo che puo' essere chiamata anche in uno statement in cui la
signature attesa vuole B*. Nell'istruzione:
b->bar();
il lookup della signature e' fatto con B::bar() a tutti gli effetti. E il
puntatore all'oggetto e' un B*. Il meccanismo di virtualizzazione si occupa
proprio di questo. A compile time tutto avviene esattamente come se tu chiamassi
effettivamente B::bar() con this di tipo B*, solo che una volta che il
compilatore ha accettato la chiamata come valida, genera un codice particolare
che *A RUNTIME* verifica il tipo concreto dell'oggetto puntato, verifica quale
e' la funzione effettivamente da chiamare, trasforma il this di tipo B* in base
al risultato e infine invoca la funzione giusta, nel nostro caso D::bar(), anche
se la sua signature in effetti non e' quella della chiamata.
Puo' farlo proprio perche' SA (ha fatto il check a runtime) quale e' il vero
tipo dell'oggetto.
Quindi in questo caso una funzione con signature "void D::()" viene chiamata in
un contesto che a compile time e' "void B::()".


Ti ringrazio per la risposta esauriente, ero sicuro del fatto che "virtual"
forzasse il binding ma avevo dei dubbi sul meccanismo della conversione del
parametro implicito this a tempo di esecuzione (run time object mutation??).
Non avevo mai riflettuto sul fatto che, nella sostanza, definire un metodo
virtual "deve" far parte in qualche modo della segnatura, la cosa mi lascia
perplesso, e penso che la segnatura venga semplicemente aggiornata in
runtime e il fatto di definire un metodo virtuale "avvisi" sulla possibilità
che il parametro implicito del metodo possa essere convertito.
Quando avviene la conversione del parametro implicito, tu dici che avviene
un controllo del tipo dell'oggetto puntato e successivamente viene invocata
la funzione corretta, ora ho sempre pensato che il type checking avvenisse
solo nel momento della dichiarazione e non al momento dell'utilizzo (come
avviene in smalltalk, clos...), che esista anche in questo caso un
meccanismo di type checking ritardato??
Cordialmente, Andrea.

 meccanismo di type checking ritardato??
Il controllo non avviene in modo esplicito. Il compilatore costruisce un
meccanismo che provochi automaticamente la chiamata della funzione
giusta. Il meccanismo piu' comune (anzi, non sono a conoscenza di altri)
e' quello della vtable. Ogni oggetto di una classe contenente funzioni
membro virtuali contiene un puntatore ad una tabella di puntatori a
funzioni, che contiene un puntatore per ogni funzione membro virtuale.
La costruzione della tabella di una sottoclasse e' effettuata copiando
la tabella della superclasse e sostituendo i puntatori relativi a
funzioni ridefinite con quelli della classe figlia (ed aggiungendo
eventuali puntatori relativi a nuove funzioni membro virtuali).
In questo modo la chiamata a una funzione membro virtuale e' risolta
semplicemente indicizzando il puntatore alla vtable.
Ciao,
Nicola

 meccanismo di type checking ritardato??
In realtà non avviene un controllo nel senso classico (infatti non serve che ci
sia il RTTI). Pero' il meccanismo di implementazione e' tale per cui "e' come se
avvenisse un controllo". In realta' quello che succede spesso e' che viene
invocato del codice nascosto che sa a priori che tipo di trasformazione deve
fare e che la esegue prima di eseguire la vera e propria funzione.
A basso livello funziona cosi':
Ogni classe che possiede dei metodi virtuali ha una specie di membro dati
privato nascosto che si trova sempre all'inizio (come fosse il primo elemento
della struct) e che quindi ha come indirizzo l'indirizzo stesso dell'oggetto.
Questo membro e' un puntatore ad una "virtual table". La virtual table (una per
ogni classe che possiede funzioni virtuali) e' una tabella di puntatori che
puntano appunto al codice da eseguire per le varie funzioni.
Per esempio:
class B {
public:
    void foo();
    virtual ~B();
protected:
    virtual void bar1();
    virtual void bar2();
private:
    int i;
};
class D: public B {
public:
    virtual ~D();
protected:
    virtual void bar1();
    virtual void bar3();
};
diventa qualche cosa del genere (nota che la sintassi non e' corretta):
struct __vtbl_B_t {
    void (B::*__p_bar1)();
    void (B::*__p_bar2)();
    __dtr (B::*__p_dtr)();
};
struct B {
    __vtbl_B_t *__pvt;
    int i;
};
__vtbl_B_t __vtbl_B = {
    &B::bar1(),
    &B::bar2(),
    &B::~B()
};
struct __vtbl_D_t {
    void (D::*__p_bar1)();
    void (D::*__p_bar2)();
    void (D::*__p_bar3)();
    __dtr D::*__p_dtr();
};
struct D {
    __vtbl_D_t *__pvt;
    int i;
};
__vtbl_D_t __vtbl_D = {
    &D::bar1(),
    &B::bar2(),
    &D::bar3(),
    &D::~D()
};
Ogni volta che un oggetto viene creato i suoi costruttori settano anche il primo
elemento (che e' un puntatore e quindi ha sempre la stessa dimensione
indipendentemente dall'oggetto) alla tabella corretta. Nota che in questo
semplice caso non esiste differenza di indirizzo fra un puntatore a D e un
puntatore a B (che fa parte del D). Pertanto, fregandosene del type system, il
meccanismo delle tabelle e la chiamata tramite jump table e' sufficiente a fare
tutto il lavoro.
Le cose si complicano se c'e' ereditarietà multipla. Per esempio:
struct A {
    int i;
};
struct DD: public A, bublic B {
public:
    virtual void ~DD();
protected:
    virtual void bar1();
};
In questo caso la struttura di DD e':
struct A {
    int i;
};
struct DD {
    __vtbl_DD_t *__pvt;
    A __a;  // { int i; }
    B __b;  // { __vtbl_B_t *__pvt;
            //   int i; }
};
Nota che l'indirizzo di un DD e' diverso dall'indirizo di un A che ne fa parte e
da quello di un B che ne fa parte. Nota anche che ci sono DUE puntatori a
virtual table.
Quello che avviene di solito e' che per la classe DD vengono generate due
diverse tabelle. Fatte in questo modo:
struct __vtbl_B_in_DD_t {
    void (B::*__p_bar1)();
    void (B::*__p_bar2)();
    __dtr (B::*__p_dtr)();
};
struct __vtbl_DD_t {
    void (DD::*__p_bar1)();
    void (DD::*__p_bar2)();
    __dtr (DD::*__p_dtr)();
};
il problema ora e' inizializzarle. Nota che Per farlo vengono creati degli stub
di conversione. Ovvero degli entry point alternativi alle varie funzioni. Si
tratta di cose non esprimibili in C/C++ ma solo in assembler. In pratica questi
entry point gestiscono la conversione del parametro this in ingresso e (in
alcuni casi) parte della preparazione dello stack frame, poi eseguono un JUMP
all'interno del codice della vera funzione bypassando, se necessario, la parte
di creazione dello stack frame. Nel nostro caso ne verrebbero creati 3 e avremmo
qualche cosa del tipo:
__vtbl_B_in_DD_t __vtbl_B_in_DD = {
    &B_bar1_stub_to_DD_bar1(),
    &B::bar2(),
    &B_dtr_stub_to_DD_dtr()
};
__vtbl_DD_t __vtbl_DD = {
    &DD::bar1(),
    &DD_bar2_stub_to_B_bar2(),
    &DD::~DD()
};


 bypassando, se necessario, la parte di creazione dello stack frame.
Stub?? ...Intendi un'adattatore di impedenza sulle linee di trasmissione?
;-)
Ho intuito quello che vuoi dire, ma non conosco questa parte per potere
rispondere in modo sensato alla tua affermazione, comunque penso di essermi
chiarito a sufficienza le idee a proposito del quesito postato inizialmente,
ti ringrazio per i chiarimenti e gli esempi abbondanti.
Cordialmente, Andrea.

 semplicemente indicizzando il puntatore alla vtable.
Rendendo il meccanismo efficiente quanto una chiamata a funzione. :-)
Cordialmente, Andrea.

 ti ringrazio per i chiarimenti e gli esempi abbondanti.
In PSEUDO assembler il codice di una virtuale e' qualche cosa del tipo:
; function entry point
label: virtualFunctionName
    ; setup stack-frame
    ...
    ; fix calling convention stuff
    ...
    ; actual code
    ...
    ; clenup stack frame
    ...
    ret
nel caso esistano uno stub (o anche piu' di uno) abbiamo entry point multipli:
; stub entry point
label: virtualFunctionName_stub
    ; setup stack-frame
    ...
    ; fix calling convention stuff
    ...
    ; convert this as needed
    ...
    JMP virtualFunctionName_code
; function entry point
label: virtualFunctionName
    ; setup stack-frame
    ...
    ; fix calling convention stuff
    ...
label: virtualFunctionName_code
    ; actual code
    ...
    ; clenup stack frame
    ...
    ret


 quella classe (ma non di quelle da essa derivate).
Right, qualche volta devo tenere a bada l'immaginazione, ho pensato che
esistesse un grafo orientato di vtbl corrispondente alla gerarchia delle
classi derivate. Pensandoci bene non mi pare abbia una grande utilità...
[...]
Il codice che hai postato mi ha chiarito ulteriormente le idee.
P.S. Considerata la velocità con cui riesci a scrivere righe di codice, in
azienda devi avere sballato le metriche di produttività!!  ;-)
Thanks again, Andrea.

 azienda devi avere sballato le metriche di produttività!!  ;-)
huh? In realtà, pur essendo storicamente uno sviluppatore qui in urka mi occupo
della parte tecnica (praticamente amministratore di sistema) e della
realizzazione di DVD (codifica, authoring e premastering).
La cosa piu' simile al codice che scrivo sono i comandi di navigazione dei menu
dei DVD!


 azienda devi avere sballato le metriche di produttività!!  ;-)
:-D

 Rendendo il meccanismo efficiente quanto una chiamata a funzione. :-)
non proprio. la vtable rende il meccanismo efficiente quanto una chiamata 
attravarso un puntatore a funzione, quindi un livelli di redirezione in 
piu'.
Ciao Anto
-- 
Antonio Cuni, 19 anni, studente
Brainbench C++ Master: 4.6, higher than 98% of all previous test takers.
C Language: 3.32, higher than 79% of all previous test takers.
Guida C/C++ di Programmazione.it: 
http://cpp.programmazione.it

piu'.
Veramente la vtable classica comporta *due* indirezioni
nelle chiamate a metodo:
  - Dato l'oggetto si trova la vtable
  - Data la vtable si trova la funzione
Esprimere in C la chiamata "window->DrawTo(x,y)"
supponendo che si tratti di un metodo virtuale C++
porterebbe a qualcosa del tipo
  (window->__vtab[DrawTo__vii]) // recupera ptr
  (window,x,y);                 // chiama
Utilizzare un puntatore a funzione direttamente
nell'istanza invece porterebbe ad avere
  (window->DrawTo__vii)         // recupera ptr
  (window,x,y);                 // chiama
Questo (senza vtab) e' il sistema che uso di
solito per scrivere codice a oggetti in C.
Andrea
--
Andrea Griffini, Programmer
http://www.netservers.com/~agriff