Newsgroup: it.comp.lang.c++
Titolo: Puntatori e polimorfismo

Stasera ahime' ho fatto una atroce scoperta, a cui non pensavo ma che
in effetti poteva essere cosi': io ho
class Ast    astratta
class Conc   implementazione di Ast
ora io volevo fare una cosa del genere: Ast *p= new Conc[10] e fino qui
tutto ok. Poi pero' per scorrere questo array dinamico ma come faccio?
Non funziona piu' p[10] o p++ perche' il compilatore va avanti della
dimensione dell'oggetto Ast (4 byte, il vptr) e non di quello Conc..
Ora, lo so che esiste il vector, ma mi spiegate come si itera nell'array
in una situazione di questo tipo, se e' possibile? N.B.: la funzione
che deve iterare non conosce a priori di che tipo sono gli oggetti allocati,
sa solo che sono di classe Ast, e oltre a iterare deve chiamare anche
dei metodi degli oggetti allocati, tipo p[0].print().
-- 
Saluti,
 Francesco Cipriani

in una situazione di questo tipo,
Lasciando perdere l'uso di std::vector (preferibile), avresti dovuto
scrivere
Ast** p = new Ast*[10];
a questo punto avresti avuto un array di puntatori (da
allocare) e p[i] ti avrebbe restituito un puntatore.
Ci sarebbero anche altre soluzioni (piu` complicate), ma la
migliore (da tutti i punti di vista) sarebbe l'uso di std::vector.
Paolo
--------------------------------------------------
  Paolo Marotta
  Computer Science, University of Pisa
  home:   
http://www.cli.di.unipi.it/~marotta
--------------------------------------------------

 dei metodi degli oggetti allocati, tipo p[0].print().
La funzione in questione deve accettare fra i suoi argomenti anche
la dimensione (sizeof) di ciascun elemento dell'array su cui deve
iterare; essa deve inoltre ricorrere a costrutti di livello ancora piu`
basso per effettuare questa iterazione.  Ad esempio:
class Ast {
public:
    void print();
};
void printali(Ast* primo, size_t numels, size_t elsize)
{
    typedef unsigned char byte;
    while(numels--) {
        primo->print();
        byte* ptemp = reinterpret_cast<byte*>(primo);
        ptemp += elsize;
        primo = reinterpret_cast<Ast*>(ptemp);
    }
}
Bello, nevvero?  In linguaggio-macchina esadecimale si
avrebbe forse codice piu` chiaro e ad alto livello...!-)
Se si deve lavorare spesso a questi livelli "infimi" (nel
senso di superlativo di "bassi"!-) conviene incapsulare
l'operazione di aritmetica degli indirizzi, cosi` come lo
si farebbe in Assembler con una buona vecchia macro;
in C++ si puo` farlo con un piu` elegante template.
Ad esempio:
template<typename T>
inline T* index(T* primo, int n, size_t elsize)
{
    typedef unsigned char byte;
    byte* ptemp = reinterpret_cast<byte*>(primo);
    return reinterpret_cast<T*>(ptemp+n*elsize);
}
(l'argomento n deve essere int per poter applicare
correttamente eventuali offset negativi).
Avendo a disposizione questo template, la printali()
si puo` riscrivere in modo un po' meno orrendo...:
void printali(Ast* primo, size_t numels, size_t elsize)
{
    for(size_t i = 0; i<numels; ++i)
        index(primo, i, elsize)->print();
}
ma non bisogna scordare che questo e` in buona
sostanza un "nascondere la spazzatura sotto il
tappeto": non fa DAVVERO pulizia, si limita a
CELARE la sporcizia (l'insicurezza di quei cast e
di quella aritmetica d'indirizzi esplicita...).
Nondimeno, la soluzione e` sicuramente superiore
a quella basata su cast e aritmetica d'indirizzi in
chiaro nel programma, e un minimo di solidita` in
piu` (congruenza garantita fra tipi di input e tipi
di ritorno) il template la garantisce (non e`, come
una macro, una manipolazione PURAMENTE a
livello testuale: ha un certo grado di integrazione
con il "sistema dei tipi" del linguaggio).  In questo
senso, questo passaggio puo` anche essere visto
come una ulteriore dimostrazione della enorme
potenza del linguaggio C++, nei casi in cui esso
"serve davvero".
Alex

Ast** p = new Ast*[10];
In effetti ci avevo pensato, ma speravo esistesse qualche soluzione
piu' facile, che pero', come Alex mi fa notare, non esiste con gli
array, quindi come non detto e ben venga lo std::vector.
-- 
Saluti,
 Francesco Cipriani

bassissimo livello con altri come quello di eredita`.
Dunque, vedendo il metodo che avrei dovuto usare ho ora optato per
il vector, sai com'e' :), ma il problema piu' o meno rimane:
sempre con le classi Ast e Conc di cui sopra, se io faccio
vector<Ast> *vec;
vec = new vector<Conc>;
il compilatore da errore, in quando mi dice che vector<Ast> non e' un tipo
base per vector<Conc> .. e quindi anche usando un vector devo usare
qualcosa tipo vector<Ast*> ? Non vedo quindi un gran guadagno rispetto
un array...
-- 
Saluti,
 Francesco Cipriani

 tutto ok. Poi pero' per scorrere questo array dinamico ma come faccio?
Devi utilizzare un array di puntatori ad Ast, magari allocato
dinamicamente, non un array di oggetti Ast. Es.:
Ast **p = new Ast * [ 10 ];
e poi
for ( int i = 0; i < 10; i++ )
    p[ i ] = new Conc;
A questo punto l'accesso ai metodi sarà come
p[ i ] -> print();
Ricordati di rilasciare la memoria con
for ( int i = 0; i < 10; i++ )
    delete p[ i ];
delete [] p;
EMG

 avrebbe forse codice piu` chiaro e ad alto livello...!-)
Decisamente chiunque legga questo tuo messaggio non userà mai più i
puntatori. :-)
EMG

 un array...
Per forza. Stai continuando ad utilizzare gli array. Hai fatto un fritto
misto. :-) A cosa mai ti serve un puntatore a vector<Ast>? Non basta il
vector<Ast> stesso? Non devi far altro che riempirlo con gli elementi
che vuoi usando gli opportuni metodi (push_back()). E non devi quindi
utilizzare l'operatore new che, ovviamente, sarà gestito internamente e
segretamente dal vector stesso.
EMG

 che vuoi usando gli opportuni metodi (push_back()). E non devi quindi
Non credo proprio che possa esistere un vector<Ast> se Ast e`
una classe astratta, ti pare...?  E se fosse concreta, avremmo
una serie di "slicing", uno a ciascun push_back -- eeeck...!-)
Attenzione che NON si puo` fare neppure un
std::vector< std::auto_ptr<Ast> >
e questo perche` l'auto_ptr non e` classe canonica (l'assignment
ha effetti collaterali...) e quindi non puo` vivere come parametro
di un contenitore standard C++.
In alternativa all'approccio polimorfico che ho delineato nel mio
ultimo (e pesantuccio:-) post, un altro forse piu` semplice per
questo specifico caso sarebbe quello di usare un idioma letter/
envelope.  Richiede, in generale, che Ast sia clonabile (cioe` che
esiste fra i suoi membri un virtual Ast* clone() const = 0, o
simili) o per maggiore efficienza che supporti un reference
count (ad esempio che fra i suoi membri ci siano una
virtual void AddRef() = 0, e una virtual void Release() = 0);
questo, se e solo se si vuole che il "vettore di buste" curi
tutti i "particolari" di lifetime degli oggetti in gioco, ovvio.
Se interessa, posso sviluppare il tema, fatemi sapere (qui sul
gruppo, NON nella mia e-mail, grazie).
Alex

 un array...
Il concetto sarebbe qualcosa del tipo:
template <typename T>
void stampali_tutti(T begin, T end)
{
    while(begin != end) {
        begin->stampa_questo();
        ++begin;
    }
}
Questa e` una funzione generica che puoi chiamare come
    stampali_tutti(vec->begin(), vec->end());
(a parte che hai un livello di indirettezza in piu` del tutto
inutile con quella "new vector<Conc>" -- puoi benissimo
dichiarare vector<Conc> vec).
In altri termini, i template servono anzitutto alla genericita`,
anche se questa, di per se`, non conflitta (anzi!) con il
polimorfismo.
Se per qualche ragione non puoi o non vuoi avere una
funzione generica, bensi` DEVI incapsularla in termini
stretti di polimorfismo (ad esempio, in alcuni ambienti si
puo` usare il polimorfismo, ma non la genericita`, fra
thread separati, ovvero fra componenti separati nel senso
di DLL o altre forme di librerie dinamiche), allora devi,
appunto, _progettare bene_ il polimorfismo che ti serve.
Considera "un puntatore ad X".  In C++, questo costrutto
di basso livello congiunge DUE ruoli che intrinsecamente
potrebbero/dovrebbero essere separati...:
a) indicare "l'oggetto X corrente"; in QUESTO senso si
    ha il polimorfismo, poiche` questa indicazione di
    "oggetto di classe X" si puo` utilizzare dovunque sia
    richiesto un "oggetto di classe Y" per qualsiasi classe
    Y che sia antenata della classe X;
b) permetterti di "passare all'oggetto X successivo", usando
    il concetto di "aritmetica degli indirizzi" (in realta` puoi
    fare di piu` del semplice "passare al successivo", in
    quanto l'aritmetica degli indirizzi e` piu` generale, ma
    prendiamo questo come il caso piu` semplice e di vasto
    interesse -- il "forward iterator" delle librerie standard).
Nel senso (b) il puntatore NON supporta il polimorfismo,
in quanto la nozione (compile-time) di QUALE sia la
esatta classe X e` indispensabile al compilatore per
effettuare internamente le operazioni di "scalaggio" che
sono implicite nell'aritmetica degli indirizzi.  Quindi, se
passi il "puntatore ad X" in punti ove e` richiesto un
"puntatore ad Y" per qualche Y antenata di X, puoi si`
usare l'"oggetto corrente", ma NON puoi piu` in modo
corretto "passare al successivo" (e il compilatore NON se
ne accorge, e questo e` uno dei tanti meccanismi per
cui i puntatori costituiscono frequente causa di errori).
La genericita` "ricattura" l'indispensabile feature (b)
incapsulandola nel concetto di "iteratore" (specificamente,
iteratore "forward" se ci interessa solo l'operator++ per
"passare all'elemento successivo"); ma la genericita`
funziona appieno se e solo se tu la mantieni (cioe`
rendi generica anche la funzione che deve ricevere
oggetti generici...!).  Per ipotesi, possiamo invece dire
di trovarci in un caso in cui, "all'interfaccia" (ad esempio
fra componenti separati), la genericita` non puo` essere
"propagata", e quindi vogliamo/dobbiamo riportare
anche (b) a termini di polimorfismo.  OK?
Ebbene, se dobbiamo lavorare con il polimorfismo, vuol
dire che ci serve un'interfaccia astratta _che incapsuli
le funzionalita` di cui abbiamo bisogno_.  Questa, a sua
volta e per generalita`, puo` essere generica, ma non e`
necessario _propagare_ la sua genericita`, e` solo una
questione di comodita` implementativa.
A proposito, le classi (interfacce, o concrete) che incapsulano
parte (o tutto) delle funzioni di un "puntatore", ma ci tolgono
dai piedi parte o tutto del dannato "basso livello" del nudo
e crudo puntatore vero e proprio, si chiamano di solito
"smart pointer" ("puntatori furbi").  E` dunque uno
"smart pointer" l'interfaccia astratta da progettare qui.
Per fortuna, non e` per nulla difficile progettarla...:
template <class T>
class my_forward_iterator {
public:
    virtual T& operator->() = 0;
    virtual void operator++() = 0;
};
e neppure e` minimamente difficile implementarla,
basta delegare a un qualsiasi decente iterator esistente
(compresi i puntatori!-):
template <class T, class P>
class my_forward_iterator_impl: public my_forward_iterator<T> {
    P p;
public:
    my_forward_iterator_impl(P p): p(p) {}
    virtual T& operator->() { return p.operator->(); }
    virtual void operator++() { ++p; }
};
Se vuoi piu` "campanelli e fischietti" ("bells and whistles":-),
non ti sara` difficile aggiungerli, ma l'essenziale e` tutto
qui (ci sono alcune sottigliezze di const-correctness, ma
non vorrei adesso mettermi a parlare qui anche di
mutable, mi sembra che di carne al fuoco ne abbiamo
messa abbastanza per un post:-).
Allora, il codice-cliente sara` (nota che NON e` generico,
perche` usa una SPECIFICA ISTANZIAZIONE del template;
usa invece il _polimorfismo_ di my_forward_iterator e quello
di Ast...!):
void stampa_tanti_ast(int quanti,
       my_forward_iterator<Ast>& p)
{
    for(int i=0; i<quanti; ++i) {
        p->print();
        ++p;
    }
}
e per chiamarlo, il codice che ha un std::vector<Conc>
potra` fare:
void fai_il_furbo(std::vector<Conc>& vec)
{
    typedef std::vector<Conc>::iterator iter_t;
    typedef my_forward_iterator_impl<Ast, iter_t> fw_t;
    fw_t iter(vec.begin());
    stampa_tanti_ast(vec.size(), iter);
}
Nota che NON pretendiamo mai di assegnare (via
puntatori o riferimenti) un contenitore-di-X ad un
contenitore-di-Y, anche se X is-A Y, perche` lo is-A
***NON*** si propaga: e questo non e` un "problema
del C++", bensi` un problema profondo e totalmente
ineliminabile dell'object-oriented.
Ad esempio: un sacco-di-banane NON-E`-UN
sacco-di-frutti, anche se una banana E`-UN frutto;
infatti, un sacco-di-frutti e` qualcosa in cui posso
infilare frutti qualsiasi, il suo membro add() prende
come argomento un frutto&, mentre un sacco-di-banane
e` quello in cui posso infilare SOLO banane, quindi
NON e` usabile-come-un sacco-di-frutti...
La questione e` piu` raffinata sotto ipotesi di
immutabilita`, cioe` quella questione const/mutable
che qui spero appunto di evitare...:-)...:-(.
Comunque, chiunque avesse ancora dei dubbi relativamente
alle mie frequenti asserzioni che il C++ e` il piu` complicato
dei linguaggi (nonche` di potenza smisurata) spero che, con
questo post, i dubbi in merito se li sia tolti!-)
Alex

Cosa c'e' che non va in std::vector<Ast *> ?
Ciao,
Nicola

che vuoi usando gli opportuni metodi (push_back()).
Si ma il problema e' che io dentro vector<Ast> ci devo mettere dei
Conc e non mi dire di usare un vector<Conc> perche' Conc puo' essere
Conc1 o Conc2, dove Conc1 e Conc2 sono entrambi derivati dalla classe
base Ast. Ora mi leggero' con calma il post di Alex (mi impressiona
solo vedere quanto e' lungo :)) )
-- 
Saluti,
 Francesco Cipriani

una serie di "slicing", uno a ciascun push_back -- eeeck...!-)
La cosa strna infatti e' che al compilatore sta bene la dichiarazione
quando poi sa che non potra' mai esistere un vector<Ast>...
E Ast ha dei metodi virtuali puri..
-- 
Saluti,
 Francesco Cipriani

Francesco Cipriani <f...@news.localhost> wrote in message
c1.2b8.2V73k8$1...@news.nwobbs...

Penso sia un problema relativo a QUANDO il compilatore debba
controllare le istanziazioni di template -- tempo di dichiarazione
rispetto a tempo di istanziazione (sempre durante il compile
time, ma concettualmente separati).  E` una delle parti piu`
sottili dello standard e non sono sicuro che il compilatore sia
in errore in questo caso...
Alex

 solo vedere quanto e' lungo :)) )
L'approccio migliore per il tuo specifico problema e`, secondo
me, uno std::vector< envelope<Ast> >.  Naturalmente occorre
definire bene lo smart-pointer "envelope<>", ma questo non
e` arduo purche` Ast abbia, o un metodo clone(), o una
gestione del reference-count (si puo` anche usare un RC
separato ma comincia a diventare un po' oneroso e meno
semplice).
Ad esempio, se Ast ha un virtual Ast* clone() const = 0,
(e ovviamente un virtual ~Ast()!!!), allora:
template<class T>
class envelope {
    T* p;
public:
    envelope(T* p=0): p(p) {}
    envelope(const envelope& e): p(e.p->clone()) {}
    ~envelope() { delete p; }
    void operator=(const envelope& e) {
        if(&e != this) {
            delete p;
            p = e.p->clone();
        }
    }
    T* operator->() { return p; }
};
e mi pare che l'essenziale ci sia... adesso:
std::vector< envelope<Ast> > vec;
vec.push_back(new Conc1);
vec.push_back(new Conc2);
vec[0]->print();
vec[1]->print();
dovrebbe essere tutto a posto (e nessuna
fatica o preoccupazione di delete ecc...).
L'implementazione se Ast ha Addref e
Release invece di clone (+ efficiente)
e` lasciata come esercizio per il lettore:-).
Alex

 Cosa c'e' che non va in std::vector<Ast *> ?
I puntatori nudi che contiene, con relative rogne
di appartenenza, cancellazione, ecc.
Alex

 di un contenitore standard C++.
Ma una cosa come quella che riporto appresso (a parte la sua assoluta
inutilitą) ha qualche problema?
#include <iostream>
#include <vector>
using namespace std;
class A
{
    private:
        float f;
    public:
        A() {}
        A( float f ): f( f ) {}
        virtual void ChiSono() { cout << "Sono A " << f << endl; }
};
class B: public A
{
    private:
        float f;
    public:
        B() {}
        B( float f ): A( f ), f( f ) {}
        void ChiSono() { cout << "Sono B " << f << endl; }
};
int main()
{
    vector < A * > va;
    A a( 123.456 );
    B b( 654.321 );
    va.push_back( &a );
    va.push_back( &b );
    for ( unsigned int i = 0; i < va.size(); i++ )
        va[ i ] -> ChiSono();
    // Oppure
    for ( vector < A * >::const_iterator i = va.begin(); i != va.end();
++i )
        ( *i ) -> ChiSono();
}
EMG

 solo vedere quanto e' lungo :)) )
Hai ragione! :-)
EMG

     va.push_back( &b );
cioe` sono tutti variabili auto, per cui i relativi distruttori
automagicamente funzionano a puntino.
Magari si potesse lavorare sempre e solo con variabili
auto e con l'annidamento perfetto (contenitore che
'muore' simultaneamente ai suoi contenuti, nessuna
necessita` di allocare e liberare esplicitamente): allora
Java non avrebbe in pratica sostanziali vantaggi rispetto
a C++, visto che l'automatizzazione della garbage
collection (con eliminazione dei puntatori dal linguaggio)
e` proprio uno dei punti-chiave di Java...!
Di solito, non e` cosi` semplice; vogliamo, o meglio
dobbiamo, occuparci noi, esplicitamente oppure
implicitamente, dei lifetime dei contenuti.  Farlo in
modo esplicito a colpi di delete presenta non pochi
problemi (anche se ci si ricorda l'indispensabile dtor
virtuale per la classe-base:-).  Gli smart pointer di
vari tipi, compreso il particolare l'idioma che e` detto
letter/envelope, servono appunto ad evitare questo
aggravio della gestione esplicita del lifetime...
Alex


Beh, si, direi che ci sono due strade fondamentali per ovviare, cioe'
mettere qualcosa intorno a "Ast *" dentro il vettore, o mettere qualcosa
intorno all'intero vettore.
Per altro concordo col tuo punto di vista, espresso altrove, che i
puntatori siano la principale causa di errori nella programmazione in
C/C++ (e non solo).
Pero' mi pare che non siano sempre evitabili e non ho ancora trovato
soluzioni che mi soddisfino pienamente circa la loro gestione. Mi pare
che tutti i modi (leggi pattern, idiomi, ecc.) che conosco per arginarne
la pericolosita' abbiano vantaggi e svantaggi.
Ciao,
Nicola

 la pericolosita' abbiano vantaggi e svantaggi.
Certamente.  Anche Java puo` essere visto come una soluzione
(un po' estrema:-) a questo specifico problema, e anch'esso ha
vantaggi (sviluppo assai piu` rapido) e svantaggi (un 20-30% di
overhead in applicazioni tipiche, probabilmente ineliminabile,
per la garbage collection & similia).
Alex

Piu' il fatto che la relativamente recente aggiunta dei weak references
sembra indicare che, almeno in casi particolari, nemmeno la garbage
collection e' una panacea...
Ciao,
Nicola

 collection e' una panacea...
I weak references possono essere "necessari", che io sappia,
solo per ragioni di prestazioni (possibilita` di tenere una "cache")
in presenza di vera GC; ce n'e` piu` bisogno in un sistema a
reference-count (dove i cicli di strong-reference possono essere
deleteri... per la GC, nessun problema).
Alex

Per carita', non intendevo dire che l'unico modo di realizzare cose
serie/efficaci/ecc. in Java e' di usare weak references, solo che guarda
caso tutti gli inventori di linguaggi del tipo "Programmatore, so io
cosa e' bene per te" prima o poi cominciano ad introdurre delle botole,
sia pure per situazioni molto particolari. Probabilmente N. Wirth non
l'ha mai fatto, ma le versioni praticamente utilizzate del Pascal gliele
ha pasticciate qualcun altro...
Ciao,
Nicola

};
Ma praticamente questo e' quello che fa auto_ptr o sbaglio?
O forse e' simile, visto che da quanto ho capito se assegni
un auto_ptr a un altro non viene creato un nuovo oggetto ma
l'auto_ptr gli passa l'ownership del puntatore.
[p.s.: avevo postato un mex dove chiedevo come creare un
iterator per una lista, non e' che per caso non e' arrivato?]
-- 
Saluti,
 Francesco Cipriani

 iterator per una lista, non e' che per caso non e' arrivato?]
Mah, non saprei.
Alex