Newsgroup: it.comp.lang.c++
Titolo: RTTI senza cast ?

Il problema che mi sono posto (che dal mio modesto punto di vista e'
irrisolvibile), e' quello di accedere agli elementi polimorfici di un
contenitore, SENZA usare le orribili espressioni di cast.
Nel concreto, mi trovo a che fare con l'albero sintattico di un'espressione
matematica, in cui ogni nodo ha un puntatore ad un oggetto che fa parte di
una gerarchia (funzioni, numeri, matrici, etc).
A parte alcune funzioni che posso concettualmente sistemare nella classe
base astratta, in moltissimi casi (ad esempio la semplificazione o la
derivazione), mi e' indispensabile accedere al reale tipo del mio nodo, con
espressioni del tipo :
|    if (o->Tipo() == Binario)
|    {
|        OpBin * ob = static_cast<OpBin *>(o);
|        ...
|        if (ob->Tipo() == OverLoad)
|       {
|           OpBinOvr *obo = static_cast<OpBinOvr *>(ob)
|    ...
Nell'esempio riportato non uso typeid ma una funzione virtuale Tipo() che
restituisce un oggetto di una classe della quale ho ridefinito l'operatore
di uguaglianza perche' restituisca un true per tutte le classi
gerarchicamente superiori (cosa che mi sembra piu' leggibile del tentare una
conversione dinamica e verificarne i risultati), visto che molte operazioni
vengono eseguite per 'livello di gerarchia'.
Ma anche cosi' l'eccesso di cast mi da la sensazione di stare sbagliando
qualcosa.
Ingenuamente mi piacerebbe avere una funzione virtuale Me() che restituisce
un puntatore del tipo esatto, ma questo ovviamente e' impossibile.
Non ho nessuna idea su come rendere meno pesanti le espressioni, ma la
voglia di sperimentare non mi manca ...
Andrea Murru
amu...@tin.it
http://www.pegacity.it/informatica/case/473/index.htm

un puntatore del tipo esatto, ma questo ovviamente e' impossibile.
Oh, é possibile (in C++ Standard), grazie alla covarianza dei tipi
di ritorno (VC++ non la supporta, é una delle sue principali mancanze
rispetto allo standard); in C++ Standard é corretto definire, ad esempio:
class Base {
public:
    virtual Base* Me() = 0;
};
class Deriv: public Base {
public:
    virtual Deriv* Me();
};
eccetera... ma non ci fai proprio nulla; infatti, se hai un Base*, anche
se sai per altra via che in realtá é un Deriv*, quando chiami Me() ottieni,
dal punto di vista del compilatore, comunque un Base* -- un cast da
qualche parte ci vuole per asserire compile-time la tua "conoscenza"
condizionale runtime.
Comunque, sentire questi cast come sintomo di problema ti fa onore
(e convalida la scelta del comitato di Standard di rendere BRUTTE le
espressioni di cast, per scoraggiarne l'uso...!-).
Ecco dunque qualche nota sulla pattern Visitor, che forse puoi usare.
Anzitutto, devi aggiungere una classe astratta:
    class Visitor {
    public:
        virtual void visitBinario(Binario&) = 0;
        virtual void visitUnario(Unario&) = 0;
        // ecc ecc
    };
potresti anche avere metodi tutti con lo stesso nome, in overload,
ma non ci sono particolari vantaggi e la leggibilitá (e praticitá) ne
soffre.  Puoi inoltre usare, invece che metodi puramente astratti,
metodi con una implementazione vuota (in certi casi questo é
molto comodo perché ti permette nelle classi concrete che tu
poi derivi da Visitor di fare l'override solo dei metodi "che servono
in questo caso"; nel tuo caso, non so se ti servirebbe).
Poi, aggiungi alla classe base, magari invece della Tipo (!), un
metodo astratto:
    class BaseNode {
    public:
        virtual void acceptVisitor(Visitor&) = 0;
        // ecc ecc
    };
e fai override di questo metodo in ogni classe concreta, cosí:
    class Binario: public BaseNode {
    public:
        virtual void acceptVisitor(Visitor& v) {
            v.visitBinario(*this);
        }
        // ecc ecc
    };
A questo punto, per svolgere una data operazione concreta,
non ti resta che definire una corrispondente classe concreta
che eredita da Visitor, con opportuni override dei vari metodi
visitBinario() ecc.  Lo stile piú usuale per questo tipo di cose
é avere i metodi visitQualcosa void, e senza altri parametri
oltre al nodo visitato, usando altri metodi delle classi concrete
ereditate da Visitor per settare parametri e recuperare risultati,
ma naturalmente puoi modulare lo stile secondo le esigenze.
Questo design pattern, di per sé, non si mappa in modo
particolarmente naturale a complesse strutture di ereditá,
specialmente in caso di ereditá multipla.  Ma ci sono vari
trucchi e schemi che permettono in certi casi di ovviare
anche a questo, e simili problemi.
Il punto di partenza per questi argomenti é l'eccellente
testo "Design Patterns", di Gamma et al, vera pietra miliare
della letteratura tecnica sulla progettazione a oggetti; la
rete pullula di siti interessanti -- usando un motore di
ricerca per "pattern" e "visitor", dovresti trovarne molti.
Alex

Caro Alex,
la design pattern "Visitor", mi sembra interessante, anche se un uso
'assoluto', mi pare decisamente esagerato (dovrei cambiare radicalmente
l'approccio e scrivere molto codice). Inoltre concettualmente non mi pare
ineccepibile spostare un'opereazione (ad esempio la semplificazione)
all'interno di membri di tante diverse classi, anche perche' non puo'
prescindere da una visione dell'intero albero, deve (in alcuni casi) creare
un ramo ex-novo (usando funzioni private della classe Espressione (che
deriva anche da albero) e accedere in modo massiccio (e invasivo) ad altri
nodi. In altri termini una visita (che poi e' anche una modifica) di un
certo tipo di nodo, non ha le caratteristiche che mi spingano a considerarla
una classe (ma forse e' un mio modo di pensare errato).
Ho comunque individuato alcuni rami della gerarchia (la visita dei quali mi
costa il 90% dei cast) per i quali potrei effettivamente utilizzarla. Il mio
grado di purismo ne sarebbe soddisfatto!
Ci sono pero' alcune cose che non mi sono ben chiare (piu' che altro sul C++
e non sulla design pattern) :
1- Mi sarebbe venuto spontaneo definire statici i metodi (e le eventuali
proprieta') della classe Visitor e di quelle da essa derivanti. Ma una
funzione statica non puo' essere virtuale (visto che non viene passato il
this non e' possibile ottenere un comportamento polimorfico). C'e' comunque
qualcosa che mi lascia l'amaro in bocca e vorrei sapere se ho esplorato
tutte le vie.
2- Non ho veramente capito in che senso e' virtuale la funzione Me() del tuo
esempio. Con il mio compilatore BC++B 1.0, riceverei un warning per il fatto
che la Me() della classe derivata 'nasconde' quella della classe base. Nello
standard C++ :
|class Base
|{   ...
|    virtual Base* Me() {return this;}
|};
|class Derived
|{   ...
|    virtual Derived* Me() {return this;}
|};
|...
|Base *b = new Derived();
|Derived *d = b->Me();    // Errore ? Cosa viene chiamato ?
Se non viene chiamata Derived::Me() che polimorfismo e' ?
Viene chiamata Derived::Me(), ma il risultato subisce un cast a Base* ? e se
si' che senso ha ?
Spero che possa rispondermi entro domani, visto che poi devo partire...


 tutte le vie.
Perche' definire statici i metodi della classe Visitor ? E' un approccio
errato dato che la classe Visitor e' una classe astratta da cui derivare
le classi RealVisitor nelle quali vengono definiti i metodi effettivi di
visita delle classi Nodo.
Comunque, tu dovrai avere un controller che gestira' la selezione della
RealVisitor in base alle necessita' correnti, quindi non c'e' alcun
bisogno di definire statici i metodi della Visitor.
Forse ti lascia l'amaro in bocca il difetto imputabile alla 'Visitor
Pattern', difetto non di poco conto per progetti complessi, che
introduce una dipendenza ciclica, tra la classe Visitor e le classi
derivate dalla classe Nodo, dovuta alla necessita' di avere riferimenti
reciproci. In questo caso Robert C. Martin ha definito un pattern
'Acyclic Visitor' nel quale, con l'ausilio di alcuni dynamic_cast
(sigh), viene eliminata la dipendenza ciclica.
massimo

si' che senso ha ?
Il senso e' di non _obbligare_ la classe Derived a "buttare via", sintatticamente,
delle informazioni che sono a sua disposizione (il fatto che il puntatore
tornato e' a Derived, non ad altri sottotipi di Base); se le informazioni
vanno scartate al punto della chiamata (perche` la funzione virtuale e`
chiamata attraverso un puntatore a classe-base) non e' difficile farlo
per il compilatore, ma intanto la Derived e' potenzialmente piu` utile
perche`, in altri punti in cui la stessa funzione viene chiamata attraverso
un puntatore a Derived, tutte le informazioni restano disponibili anche
a compile-time.
La funzione "Me()", naturalmente, non serve assolutamente a nulla,
ma un caso analogo, tipicamente una funzione "clone()", puo` invece
essere utilissimo.  Se la class Base definisce un "virtual Base* clone()",
allora, nel C++-non-Standard implementato da Borland e Microsoft,
tutte le classi derivate devono pure tornare Base* dalle loro versioni
di clone() -- mentre in realta', chiaramente, la Derived::clone() sta
tornando un Derived*; quindi, per clonare un Derived che so a
compile-time essere un Derived, mi devo trovare a fare un cast --
oppure devo aggiungere goffe funzioni come cloneDerived() per
tornare un Derived* (non posso chiamarle tutte clone() perche`
due funzioni non possono essere overloaded solo sul tipo di
ritorno).  In pratica, per avere tutta la possibile funzionalita' di
cloning senza cast, in un albero di eredita` di N classi, dovrei
quindi definire N funzioni virtuali tutte di nome diverso -- e qui
tra l'altro c'e' anche uno spreco di risorse, visto che le funzioni
virtuali occupano pur sempre uno slot in una virtual table (pochi
byte per classe, ma perche` mai dovremmo sprecarli?!).  Il C++
Standard elimina tutte queste assurdita` e sprechi permettendo
la covarianza del tipo di ritorno delle funzioni virtuali (nota che
questo e' un caso ben diverso dalla covarianza del tipo degli
_argomenti_, che e` matematicamente scorretta in termini di
cosiddetta "algebra di Liskov" -- la covarianza del tipo di ritorno
e' invece Liskov-typesafe).
Alex

Per il mio problema specifico, sono veramente dibattuto tra il Cyclic e
l'Acyclic Visitor (che Massimo mi ha molto gentilmente spedito in mail-box,
visto che per mio errorre gli ho risposto via mail e non sul ng).
In effetti il mio problema fondamentale (e in questo l'Acyclic e' perfetto)
e' che vorrei scrivere un Visitor solo in casi particolari o quando ne ho
voglia/tempo, senza essere costretto a ripensare tutto o a scrivere Visitors
per ogni circostanza!
Come potete osservare non si tratta di sottigliezze filosofiche (che
comunque in generale trovo uno migliori trastulli per la mente), ma di
considerazioni molto spicce.
Per quanto riguarda i metodi statici (rispondo alle obiezioni di Alex e di
Massimo) mi vengono in mente perche' considero (e' vero, per assenza di uno
stato) un Visitor una classe e non un oggetto : per me non ha senso (o
meglio, trovo una forzatura) istanziarlo.
Due istanze di una tale classe sono la stessa cosa e non sono oggetti
diversi (concettualmente) !
Riesco pero' a concepire (in astratto) un polimorfismo di classe e non di
oggetti. Un'eresia ? Probabile, comunque penso che a questo punto sia
facilissimo infognarsi in definizioni di oggetti, classi, tipi, dati
astratti e via discorrendo, e non so quanto possa essere utile (pochino...).
Comunque non mi sembra che sarebbe troppo difficile implementare un tale
comportamento nel linguaggio.
Per il momento ringrazio tutti i miei interlocutori.
P.S. Per il prossimo periodo saro' irraggiungibile.


comportamento nel linguaggio.
Si puo` certo considerare la classe di un oggetto come oggetto
a sua volta (istanza di una "metaclasse"), parecchi linguaggi OO
lo fanno, ma la utilita' di questo approccio e' sostanzialmente nel
facilitare a programmi scritti in quel linguaggio di "manipolare"
(analizzare, ecc ecc) se stessi, o altri programmi scritti nello
stesso linguaggio -- sarebbe un approccio proprio balzano quello
di utilizzare simili meta-strumenti, per implementare un banalissimo
caso di polimorfismo che non e' "meta" per nulla.  Per fare un
esempio concreto, pensa a Java, e prova a dirci come estenderesti
quel linguaggio, che ha la sua classe Class, ecc ecc, perche`
potesse toglierti lo sfizio di implementare Visitor con il tuo
"polimorfismo di classi" invece di un normalissimo polimorfismo
(il famoso "cannone da mosche"...?).
(E comunque, la distinzione identita'/stato e' particolarmente
evidente in Java -- pensa alla distinzione fra == ed equals...!)
Alex