Newsgroup: it.comp.lang.c++
Titolo: Metodi regolari e virtuali...

Salve, sto provvedendo a costruire un nuovo componente con C++ Builder
4 e necessito di chiarimenti su un punto.
Se io ho 
class Base {
      public: 
              void          Regolare() ;
              virtual void  Virtuale() ;
} ;
class Derivata {
      public:
              void          Regolare() ;
              virtual void  Virtuale() ;
} ;
e costruisco la seguente funzione :    
void my_function( Base * B ) 
{
     B->Regolare() ;
     B->Virtuale() ;
}
Passando a my_function() un puntatore a una classe Derivata, viene
ovviamente chiamato il metodo virtuale di quest'ultima e il metodo
regolare della classe madre. E' chiaro che questo accade perche' il
binding dei metodi regolari e' statico e quello di quelli virtuali
dinamico, ma non riesco a capire che cosa accade realmente a livello
di indirizzi delle funzioni. Cioe', perche' se il metodo regolare di
Derivata sostituisce quello della classe madre, quando passo il
puntatore a un'istanza di Derivata alla funzione, mi riprende quello
della classe madre ? Derivata ha un riferimento al vecchio indirizzo ?
Mi rispondete anche per e-mail ?
--
Andrea Laforgia (
andrea_...@bigfoot.com
)
---------------------------------------------

 Mi rispondete anche per e-mail ?
Fatto (Cc in email).
Alex

Mi rispondete anche per e-mail ?
La risoluzione dei riferimenti a metodi virtuali viene effettuata
con del codice che accedendo alla VTABLE dell'oggetto
recupera a run time l'indirizzo effettivo della funzione virtuale,
in tal modo viene sempre chiamata la funzione corretta.
Nel caso di normali funzioni membro, la chiamata viene
sempre risolta staticamente. Di conseguenza se chiami
un certo metodo tramite un puntatore, per non
commettere errori, il compilatore effettua sempre la
chiamata del metodo corrispondente al tipo del puntatore (poniamo T),
senza verificare se l'oggetto puntato appartiene invece ad
una classe derivata da T.
Se usi la notazione del punto tutto cio` non crea alcun problema,
perche` il tipo dell'istanza e` evidente.
Per un approfondimento ti rimando ad un qualsiasi libro
sul C++, oppure trovi una spiegazione piu` dettagliata
nel mio tutorial (lo trovi alla mia home page
oppure su 
www.programmazione.it
 da cui e`
anche scaricabile come un unico file zip).
Paolo
-----------------------------------------------
  Paolo Marotta
  Computer Science, University of Pisa
  home:   
http://www.cli.di.unipi.it/~marotta
-----------------------------------------------

nulla in gioco su questi metodi, quindi, se "Base* pluto",
Grazie delle risposte a te e a Paolo,
Da quanto mi dite, deduco che il termine usato da Borland ("replaces")
non sia tanto felice. Giusto ? Quello che non riuscivo a capire e'
perche' passando ad una funzione, che accetta il puntatore a una
classe base, l'indirizzo della classe derivata, il metodo regolare
chiamato sia quello della classe madre e non quello della classe
figlia. La spiegazione che mi sono dato e' che il compilatore fa si'
che l'indirizzo del metodo regolare nella classe figlia venga
"sostituito" con quello del metodo regolare della classe madre e
quindi venga "fissato" a tempo di compilazione. Mentre il "dispatch"
dei metodi virtuali, avviene in base al reperimento del giusto
indirizzo nella VTABLE, in modo dinamico, facendo uso del puntatore
alla classe passato. Sono in errore ?


figlia.
Perche` al momento della chiamata viene eseguita implicitamente
una conversione al tipo della classe base. D'altronde all'interno
del corpo della funzione il compilatore non puo` fare alcuna
assunzione circa il tipo effettivo dell'oggetto puntato, sa solo
che esso e` almeno dello stesso tipo del puntatore.
I metodi virtuali servono appunto a "rimediare" a cio` (il termine
"rimediare" lo uso per farmi capire, ma non e` in realta` un
rimedio).


 alla classe passato. Sono in errore ?
Su questa parte, no.
Per completezza: che si tratti di puntatore o reference e` lo stesso
(mentre un passaggio _per valore_, lo "slicing", cambia tutto).
Quindi, e` errato dire (come ho visto su questo thread) che cambi
qualcosa la SINTASSI usata per l'invocazione (sintassi-"punto" o
altro); le cose dipendono invece dal passaggio _polimorfico_ (per 
ptr O reference).
Alex

tipo del puntatore.
D'accordo. Questo e' chiaro.
In quanto ai metodi regolari, viene compiuto un "upcasting" (giusto ?)
e quindi viene chiamato il metodo del tipo di classe ottenuta.
In quanto ai virtuali, veniamo agli esempi di Mamma Borland :
class Base 
{
      public:
              void Regular() ;
              void AnotherRegular() ;
              virtual void Virtual() ;
} ;
class Derived : Base 
{
      public: 
              void AnotherRegular() ; 
              //replacese Base::AnotherRegular()
              void Virtual() ;
              //overrides Base::Virtual()
} ;
void FunctioOne()
{
     Derived *d = new Derived ;
 
     d->Regular() ;
     // Calling Regular() as it were a member of Derived.
     // The same as calling d->Base::Regular()
     d->AnotherRegular() ;
     // calling the redefined AnotherRegular()
     // ...the replacement for Base::AnotherRegular()
     delete d ;
}
 
void FunctionTwo( Base *b ) 
{
     b->Virtual() ;
     b->AnotherRegular() ;
}
I termini usati da Borland sono i seguenti : "the virtual mechanism of
C++ allows a method to be called depending on the class type that is
being used to invoke the method".
E poi aggiunge "In the previous example, if you were to call
FunctionTwo() with a pointer to a Derived object, the function
Derived::Virtual() would be called. The virtual mechanism dynamically
inspects the class type of the object you passed at runtime and
dispatches the appropriate method".
Dalla prima parte si capisce che il meccanismo con cui vengono
trattati i metodi virtuali in C++ e' tale che un metodo viene lanciato
in dipendenza dal tipo di classe che viene usata per invocarlo.
La seconda, invece, mi sembra contrastare con la prima, perche' la
prima mi lascia pensare che l'esecuzione del metodo lanciato in
FunctionTwo() dipende dal tipo di classe utilizzata per invocarlo, che
in questo caso e' "Base", anche quando viene passato il puntatore alla
classe derivata, in quanto quest'ultimo viene "implicitamente
convertito", mentre afferma che il compilatore ispeziona dinamicamente
il tipo di classe passata (ma come fa il compilatore a sapere che e'
stato passato un (Derived*) anziche' un (Base*) ?). 
Sono io che faccio confusione ?


(mentre un passaggio _per valore_, lo "slicing", cambia tutto).
Passare per valore una classe non vuol dire, comunque, passare un
puntatore ?
Scusatemi, ma io sono convinto che tutto cio' di cui stiamo parlando
sia basato su una estrema semplicita' (come tutto cio' che appare
difficile da capire ;) e vorrei riuscire ad agganciarla ai miei
neuroni ;)...


 Sono io che faccio confusione ?
Piu` che altro e` Borland, che confonde il *run-time type* con il
*compile-time type*.
Per un metodo virtuale, la versione chiamata dipende dal *tipo
di run-time* dell'oggetto a cui sto usando un puntatore o reference.
Per un metodo non-virtuale, la versione chiamata dipende dal *tipo
a compile-time* del puntatore o reference che sto usando.
Alex

 neuroni ;)...
Infatti non c'e` nessuna grossa complicazione.
Alex

ptr O reference).
In effetti mi ero scordato dei reference, comunque per "notazione del 
punto" intendevo dire uso esplicito dell'oggetto senza passare per
puntatore e/o reference (quindi niente oggetti polimorfi).


passare per valore, utilizzando un copy constructor.
Hai ragione, ho formulato male la domanda, quella giusta era 
"passare una classe come parametro non equivale, comunque, a passare
un puntatore ?"
Perche' leggo in fondo alla sezione del mio testo :
"Every class (and therefore every component) is really a pointer. The
status of classes as pointers becomes important when you pass a class
as a parameter. In general, you should pass classes by value rather
than by reference. The reason is that classes are already pointers,
which are references; passing a class by reference amounts to passing
a reference to a reference.".


 a reference to a reference.".
Non so di che linguaggio stiano parlando, e magari e` un
linguaggio interessante, ma NON e` certamente il C++.  In
Python, o in Smalltalk, ecc, potrei effettivamente "passare
una classe" a una funzione (anche se ovviamente in simili
linguaggi non si parla mai di "puntatori", comunque si puo`
far uso del concetto di "riferimento").  In C++, *NO*.
Come questi manuali si siano fatta la fama di essere chiari e
limpidi, come tu dici all'inizio del tuo post, e` il piu` profondo
di tutti questi misteri, secondo me.  Se parlano di "passare
una classe come parametro" _a proposito del linguaggio C++_,
non c'e` sicuramente da sorprendersi se sono riusciti a
confonderti orribilmente le idee!  O se no, stanno parlando
di tutt'altro linguaggio.
Alex

Sono io che faccio confusione ?
Credo che tu non abbia ben capito il meccanismo di early bindind
(nelle funzioni normali) e late binding (per quelle virtuali).
Il mio consiglio e` quello di leggerti un buon libro (ad esempio
quello di Eckel segnalato nelle faq) e/o dare una occhiata al mio
tutorial. Faq e tutorial li trovi su 
www.programmazione.it


 un puntatore ?"
Non so a livello macchina come sia implementata la cosa ma non mi sembra
una buona interpretazione in C++. Se così fosse, tra l'altro, dovresti
fare la stessa considerazione anche per le struct in C mentre mi sembra
che sia consuetudine passarle per puntatore e non per valore, non ti
pare?
EMG

 In C++ non si puo` "passare una classe come parametro".
Ugh! Mi era sfuggito. :-)
EMG

(nelle funzioni normali) e late binding (per quelle virtuali).
Grazie delle informazioni, comunque le possibilita' sono due, o sono
stupido io, o e' borland che si spiega in modo pessimo... ;)
Propenderei per la seconda ipotesi ! ;)


di tutt'altro linguaggio.
Grazie per tutte le risposte.
Dopo questa nostra discussione, rimango davvero deluso dai manuali
Borland. La fama di essere tra i migliori testi sulla programmazione
orientata agli oggetti se la sono guadagnata sul finire degli anni
'80, quando questo paradigma stava cominciando a diffondersi da noi e
quando il competentissimo Corrado Giustozzi su MCMicrocomputer, nelle
rubriche sul C++, ne vantava gli aspetti di chiarezza e rigorosita'.
Che sia successo qualche sconvolgimento nel passaggio a Inprise ? ;)
Cmq, di fatto, credo d'aver capito come funziona tutto il meccanismo
del binding (almeno spero), intanto ho scaricato il "Thinking in C++",
primo volume e lo sto leggendo, per cui anche se mi sbagliassi ancora,
avro' tempo di ricredermi. Io, molto semplicemente, l'ho intesa in
questi termini :
Supponendo che io sia il compilatore, mi creo una tabella interna
strutturata, per es., in questo modo :
--------------------------------------------------
CLASSE | TIPO METODO  |  NOME METODO  | INDIRIZZO 
--------------------------------------------------
 base  |   regolare   |  regolare()   | 0x0000000
--------------------------------------------------
 base  |   virtuale   |  virtuale()   | 0x0000004
--------------------------------------------------
 drvt  |   regolare   |  regolare()   | 0x0000008
--------------------------------------------------
 drvt  |   virtuale   |  virtuale()   | 0x0000010
--------------------------------------------------
Nel contempo, ogni volta che incontro un metodo virtuale 
aggiorno la vtable (c'e' ridondanza, l'ho ammetto, ma e'
per farmi capire) :
------------------------------------------------
CLASSE |   NOME METODO VIRTUALE   |   INDIRIZZO 
-------------------------------------------------
 base  |        virtuale()        |   0x0000004
-------------------------------------------------
 derv  |        virtuale()        |   0x0000010
-------------------------------------------------
quando incontro la funzione :
void my_func( base *p )
{
       p->regolare() ;
       p->virtuale() ;
}
eseguo il seguente algoritmo :
1. ottengo il valore del puntatore p
2. leggo che c'e' una chiamata ad un metodo :
   D : di che tipo e' p ?
   R : base
   interrogo la tabella interna assicurandomi
   che base abbia quel metodo
   Se non ce l'ha segnalo l'errore ...
   altrimenti continuo :
   se il tipo e' regolare, allora lo chiamo recuperando 
   l'indirizzo del metodo  dalla tabella interna e quindi creo una
   call indirizzo
   (compile-time binding) 
   altrimenti lo chiamo sfruttando la vtable nel modo seguente :
   call vtable[tipo_effettivo(p)].indirizzo 
   (run-time binding)
   tipo_effettivo() sarebbe l'implementazione del RTTI.
Ovviamente le "call" sono un po' fantasiose ma e' per fare capire il
mio modello...
In questo senso e' chiaro anche quello che intendi tu per "hiding"
cioe' "nascondere", infatti ruotando in modo fantasioso la tabella
interna del compilatore di 90 gradi in modo da osservare i metodi
disposti l'uno che copre l'altro (il primo in fondo, l'ultimo in primo
piano) e' chiaro che, effettivamente, il metodo regolare della figlia
"copre" quello della madre (non lo sostituisce affatto) e quindi lo
nasconde alla vista.


pare?
Be', che la "consuetudine", proprio perche' tale, non costituisce una
regola formale. Io discuto della equivalenza delle due cose, che, come
dice Alex, non esiste.


Propenderei per la seconda ipotesi ! ;)
Chiaramente propendo anche io per la seconda ipotesi... e
sicuramente la scelta dei termini non e` particolarmente felice.
Un motivo di piu` per non limitarsi ad una unica fonte!


 rubriche sul C++, ne vantava gli aspetti di chiarezza e rigorosita'.
Leggevo anch'io quegli articoli ma, col senno di poi, temo che fossero
molto superficiali.
EMG

 dice Alex, non esiste.
E io che ho detto? Sia in C che in C++ se passi una struct o classe la
passi per valore. Se la vuoi passare per indirizzo lo devi fare
esplicitamente.
EMG

aggiorno la vtable (c'e' ridondanza, l'ho ammetto, ma e'
                                      ^^^^
accc... nella foga ho dimenticato a casa l'italiano ! ;))


cioe' "nascondere",
[omissis]
"hiding" viene utilizzato perche` derived::regolare non sostituisce
base::regolare, ma appunto lo nasconde agli occhi di chi usa
istanze di derived senza fare uso di puntatori a "base".
Ovviamente base::regolare rispunta non appena si usa
un puntatore a "base" per accedere ad un oggetto "derived".
Con i metodi virtuali questo non accade, viene sempre
invocato il metodo piu` appropriato.


 un puntatore a "base" per accedere ad un oggetto "derived".
Oppure semplicemente chiamandola attraverso lo scope resolution
operator:
derived d;
d.regolare();           // Chiama derived::regolare
d.base::regolare();     // Chiama base::regolare
Ed analogamente con i puntatori:
derived *d = new derived;
d -> regolare();           // Chiama derived::regolare
d -> base::regolare();     // Chiama base::regolare
Questo fa proprio vedere che non c'è alcuna sostituzione ma un semplice
occultamento.
EMG

operator:
Ovviamente...
C'e` pero` una differenza... Usando il risolutore di scope si scavalca a
pie` pari tutto il problema dato che specifichi totalmente cio` che vuoi
fare. La prova sta tutta in un idioma della OOP a tutti noi sicuramente
noto:
class Base {
  public:
     void Foo();
     virtual void Foo2();
};
class Derived : public Base {
  public:
     void Foo() {     // Hiding
       Base::Foo();        // richiamo esplicito metodo di classe base
       /* ... */
     }
     virtual void Foo2() {    // ridefinizione
        Base::Foo2();    // richiamo del metodo di classe base
        /* ... */
    }
};
In sostanza il termine hiding (in questo contesto) e` relativo al fatto 
che si fa implicitamente riferimento ad uno scope, non appena gli scope
divengono espliciti il problema sparisce.


 fare.
E' vero. Quindi l'utilizzo dell'operatore di scope non dimostra un bel
niente dato che anche nel caso di metodi virtuali è possibile comunque
accedere a quelli della classe base anche se si chiamano attraverso un
puntatore alla classe derivata.
EMG