Newsgroup: it.comp.lang.c++
Titolo: Operatori e classi astratte

So come definire operatori su classi concrete, ma ho dei problemi
quando li voglio definire in una gerarchia di classi in cui ce ne
sia una base che funziona da interfaccia.
In pratica, io vorrei poter scrivere un programma come questo qui
sotto, ma il compilatore da chiaramente errore perche' la classe
D non definisce l'operatore == della B perche' il parametro e'
diverso e quindi la funzione rimane virtuale e la classe D non
puo' essere istanziata. Se pero' in D::operator== uso come
parametro (B& b) poi non posso usare una cosa tipo b.a per il
confronto perche' ovviamente B non ha alcun dato membro.
Ma quindi non si puo' proprio definire un operatore tipo lo ==
a livello di classe astratta o c'e' qualche escamotage?
Definire anche gli operatori a livello di classe astratta non
sarebbe male perche' cosi' si possono scrivere funzioni che
accettano come parametri sempre le classi astratte e non dover
ogni volta controllare se va usata la classe concreta come
parametro..
Ecco il prog.
#include <iostream>
class B {
public:
    virtual void print() = 0;
    virtual bool operator==(B&) = 0;
};
class D: public B {
    int a;
public:
    D(int _a=0): a(_a) {}
    void print() { cout << a << endl; }
    bool operator==(D& d) { return d.a == a; };
};
bool areEqual(B& b1, B& b2) {
    return b1 == b2;
}
main() {
    B *d1 = new D(1);
    B *d2 = new D(2);
    if (areEqual(d1, d2))
        cout << "Uguali" << endl;
    else
        cout << "Diversi" << endl;
    delete d1; delete d2;
}
-- 
Saluti,
 Francesco Cipriani

 };
Anzitutto, e` sicuramente sbagliato che non sia definito
const il parametro di operator==, e il metodo stesso;
e probabilmente sbagliato che non sia definito const
il metodo print.  Ricorda di definire sempre const cio`
che non viene modificato: la _mancanza_ di const
significa "questo posso avere bisogno di modificarlo"
e quindi [a] il compilatore ti dara` errore se mai vuoi
usare in quel punto un oggetto const, e [b] il lettore
umano si chiedera` che cavolo di modifiche tu voglia
mai poter fare nel corso di un confronto d'eguaglianza.
Seconda cosa, una classe fatta allo scopo di venire
derivata deve praticamente sempre avere un
distruttore virtuale.
E poi, naturalmente, come hai notato, occorre che
gli argomenti dei metodi virtuali negli override siano
*eguali*, NON covarianti, rispetto a quelli della classe
base.  Di conseguenza, non c'e` dubbio che il tuo
codice va anzitutto modificato a:
class B {
public:
    virtual void print() const = 0;
    virtual bool operator==(const B&) const = 0;
    virtual ~B() {}
};
class D: public B {
    int a;
public:
    D(int _a=0): a(_a) {}
    void print() const { cout << a << endl; }
    bool operator==(const B&) const;
};
Quanto alla semantica di D::operator==, mi
pare di capire che tu voglia questo: se l'argomento
NON e` esso stesso un D, l'eguaglianza non ci
puo` essere; se lo e`, dipende dal campo a.
Per verificare a runtime l'appartenenza effettiva
di un oggetto a un certo tipo, usa dynamic_cast.
Dunque, ad esempio:
bool D::operator==(const B& b) const
{
    const D* pOther =
        dynamic_cast<const D*>(&b);
    return (pOther!=0) && a==pOther->a;
}
Il dynamic_cast<> si presta anche ad essere male
usato (finendo per permettere dei "type-switch" che
rendono il codice fragile rispetto all'aggiunta di altri
tipi derivati), ma in questo caso (se la semantica
e` in effetti quella che sopra ipotizzavo) non c'e`
alcun rischio: se e quando si aggiungeranno altri
tipi derivati da B, D::operator== continuera` a
funzionare senza problemi, senza cambiamenti di
semantica, e senza necessita` di modificarlo.
Molto spesso, la semantica vuol essere piu` raffinata.
Ad esempio, tu, come analista e progettista, potresti
distinguere tutti i possibili discendenti di B in due
sottogruppi: quelli che possono essere "interamente
riassunti" in un valore intero, e quelli che invece non
lo permettono; e volere che l'eguaglianza esista solo
se:
    -- entrambi gli oggetti discendenti di B possono
       essere interamente riassunti in un valore intero,
       E il valore intero in questione e` eguale; oppure
    -- nessuno dei due oggetti in questione puo`
       esserlo, delegando allora la questione ad un
       altro membro funzione-virtuale (e.g. "equals").
Questo tipo di raffinato approccio potresti porlo cosi`:
class B1 {
public:
    virtual void print() const = 0;
    virtual bool equals(const B1&) const = 0;
    virtual bool asInt(int&) const = 0;
    virtual ~B1() {}
};
bool operator==(const B1&a, const B1&b)
{
    int ia, ib;
    bool ba = a.asInt(ia);
    bool bb = b.asInt(ib);
    if(ba && bb) {
        return ia==ib;
    } else if(!ba && !bb) {
        return a.equals(b);
    } else {
        return false;
    }
}
class D1: public B1 {
    int a;
public:
    D1(int _a=0): a(_a) {}
    void print() const { cout << a << endl; }
    bool equals(const B1& b) const {
        return (*this)==b;
    }
    bool asInt(int& i) const {
        i = a;
        return true;
    }
};
Nota come D::equals puo` tranquillamente
delegare all'overload (globale!) di operator==
in quanto quest'ultimo delega ad equals solo
per oggetti per cui asInt torna false.
Questo tipo di approccio, quando riflette la
semantica desiderata per il progetto, risparmia
spinosi problemi connessi al dual-dispatch.
Appunto per questo (e varie altre considerazioni
ausiliarie), si preferisce in generale definire gli
operator (con ovvie eccezioni, come operator=
ecc) come funzioni globali (free function) e, se
mai, avere membri virtuali cui l'operator stesso
puo` delegare opportunamente.
Alex

const il parametro di operator==, e il metodo stesso;
ok, grazie della segnalazione, ma ho scritto l'esempio di
corsa senza badare ai particolari. Ho tentato poi di
usare il dynamic_cast, purtroppo pero' la situazione che ho
e' piu' complessa di quella di sopra, visto che la gerarchia
dove usare il dynamic_cast e' quella famosa delle date.
Semplifichero' al massimo riportando il codice strettamente
necessario
                  IDate
                   /  \
        GDate<IDate>   ITime
         (Date)          \
                         GDate<ITime>
                            \
                            DateTime
class IDate {
public:
    virtual bool operator==(const Idate&) const = 0;
}
template <class Interfaccia>
class GDate: public Interfaccia {  ______ e' corretto il parametro??
    time_t t;                     |
public:                           V
    bool operator==(const Interfaccia& d) {
        GDate<Interfaccia> *p = dynamic_cast< GDate<Interfaccia>* >(&d);
        return (p) && (p->t == t);
    }
}
typedef GDate<IDate> Date;
class ITime {
    // varie
}
class DateTime: public GDate<ITime> {
    qui ogni volta che ad esempio restituisco un valore
    di tipo DateTime mi da errore perche' dice che non lo
    posso fare poiche' l'operator== di IDate e' virtuale,
    insomma non lo vede come implementato.
}
Insomma, in breve (si fa per dire) non so che parametro mettere
nell'operator == di GDate, perche' se metto IDate mi da un errore
nel cast quando GDate viene usata come GDate<ITime>, e cosi' come
e' adesso ritiene che l'operator == di IDate non sia implementato,
quando GDate viene appunto usato in DateTime che deriva da GDate<ITime>
Spero tu abbia la soluzione :), perche' devo dire di essermi ficcato
in una cosa _forse_ :) leggermente troppo complicata per il mio
livello attuale, anche se devo riconoscere che ha il suo fascino.
-- 
Saluti,
 Francesco Cipriani

    }
Me sciagurato! :) Scrivendo
    bool operator==(const IDate& d) {
        const GDate<Interfaccia> *p = dynamic_cast< const GDate<Interfaccia>* >(&d);
        return (p) && (p->t == t);
    }
si compila senza errori, ma mi rimane un dubbio: e' giusto usare IDate
come parametro formale quando la classe usa una derivazione generica?
Dopotutto GDate e' pensata per ereditare sempre da IDate, anche se non
direttamente, pero' ho i miei dubbi
-- 
Saluti,
 Francesco Cipriani

}
Ma.. se volessi definire un B::operator++(int),
poi nell'implementazione in D che ci scrivo? Nel test
di uguaglianza e' stato sfruttato il fatto che
se un oggetto non e' dello stesso tipo e' chiaramente
diverso, ma se voglio incrementare?? Se e' dello stesso
tipo incremento, e se non lo e'? :-?
-- 
Saluti,
 Francesco Cipriani

 tipo incremento, e se non lo e'? :-?
Di QUALE oggetto vai parlando...?  L'unico oggetto
in gioco qui (a parte il povero argomento int, che
ha poco da essere altro che un int) e` *this, il cui
tipo e` chiaramente determinato da quale override
del virtuale operator++ tu ti stia trovando a eseguire.
Alex

Francesco Cipriani <f...@news.localhost> wrote in message
c1.2b8.2VHcwq$1...@news.nwobbs...

Dipende cosa intendi.  Se vuoi poter confrontare un GDate<IPippo> con
*qualsiasi* const IDate&, indipendentemente da cosa sia IPippo purche`
erede di IDate, la cosa non e` necessariamente errata.
Alex

tipo incremento, e se non lo e'? :-?
No comment, e meno male che ci ho messo anche il parametro
int.. E' chiaro che il problema del dynamic_cast si presenta
solo in caso di parametri di tipo classe base, quindi non
nel caso dell'operator++, dove bastano le semplici funzioni
virtuali
-- 
Saluti,
 Francesco Cipriani

erede di IDate, la cosa non e` necessariamente errata.
Bene, e questo e' ok. E' sorto ahime' un altro problema :(, nella
definizione dell'operatore ++(int). In genere tale operatore
restituisce l'oggetto modificato per riferimento, ma visto che
deve restituire il valore prima della modifica, bisogna prima crearne
una copia. Pero' se definisco l'operatore in questo modo:
1- creo una variabile locale tmp di tipo class D col valore attuale
2- incremento il campo che mi interessa
3- restituisco tmp
il compilatore dice giustamente che sto restituendo un valore per
riferimento a una var locale (non oso immaginare quali casini
possano succedere). Allora come posso fare?
Ho pensato: ok, non restituisco il valore per riferimento, ma,
ovviamente :), e' sorto un nuovo problema: con riferimento alle
classi IDate ITime GDate dell'altro messaggio, ho scritto
virtual IDate IDate::operator++(int) const = 0;
peccato pero' che avessi dimenticato il lieve particolare che
la classe IDate non puo' essere passata per valore essendo
astratta.. quindi bisogna trovare il modo di restituire un
reference.
Altra soluzione: non implementare l'operatore postincremento,
ma prima di arrendermi vorrei sapere se c'e' qualche cosa che
non ho considerato.
-- 
Saluti,
 Francesco Cipriani

??? che domanda e`...?
Sorry, non so nemmeno come ho potuto scriverlo.
-- 
Saluti,
 Francesco Cipriani

 nel cast quando GDate viene usata come GDate<ITime>, e cosi' come
No, non nel cast, di sicuro; dynamic_cast e` perfettamente in
grado di navigare "lateralmente" nella gerarchia.  L'errore
deve essere nella chiamata al metodo, operator==, che come
lo hai scritto, per una GDate<ITime> puo` accettare solo
operandi di destra di classi che ereditano da ITime.
Probabilmente quello che desideri e` templatizzare anche
operator== (che non c'e` d'altra parte nessuna ragione per
avere come *metodo*, quindi potresti benissimo tenere
come funzione libera).  La doppia templatizzazione puoi farla
sia (caso semplicissimo) per la funzione libera, sia (appena
piu` avanzato...) per qualsiasi metodo non virtuale.
Alex

 non ho considerato.
operator++, operator=, operator+=, eccetera, esistono
soprattutto per comodita` sintattica a beneficio delle
"classi-valore", tipo complex<>, iteratori di vario tipo,
eccetera: classi tipicamente non polimorfiche, ne` fatte
per essere ereditate.
Alex

Restituisci per valore.
Cosa che quindi come dici piu' avanti con classi astratte
non si puo' fare, quindi niente operator ++(int), ok. Certo
avere per un tipo di oggetto operator++() e non operator++(int)
mi pare "brutto" in quanot generalmente se io so che e' disponibile
l'operatore prefisso penso che lo sia anche quello postfisso,
quasi quasi tolgo anche l'altro.
-- 
Saluti,
 Francesco Cipriani

Ciao, Francesco.
Rispondo al tuo messaggio iniziale, anche se ho letto tutto il thread
(sono le 18.00 di lunedi' 13), perche' voglio fare un discorso generale.
Mi rendo conto che detto (scritto) ora suona un po' provocatorio, ma mi
sembra che la complicazione a cui e' arrivata la discussione tra te e
Alex denota un disegno inadeguato del tuo meccanismo di gestione di data
e ora. Chiaro che come esercizio sull'applicazione di tecniche
specifiche e' ottimo, ma in un contesto "produttivo" avresti avuto meno
problemi con una classe unica (o poco piu'), che inglobasse
l'informazione di data e/o ora in uno o due attributi (il tipo puo'
dipendere da diversi aspetti), e che ricavasse da questi i diversi
elementi, come anno, mese, ecc. Questo non per dire che in generale e'
"meglio" scrivere una classe data/ora in un certo modo, quanto che il
modello migliore dipende dall'uso che si vuol fare, ovvero dai metodi
che si vogliono fornire.
Ciao,
Nicola

operandi di destra di classi che ereditano da ITime.
Gia' ecco.. alla fine ho cambiato Interfaccia in IDate che sta
sopra a tutti e ha funzionato.
Ora l'ultima domanda su questa gerarchia: se viene passato un
oggetto IDate al costruttore di copia o all'operatore = della
classe GenericDate<Interfaccia> , quest'ultima come dovrebbe
agire? Usando il dynamic_cast posso controllare se l'oggetto
passato e' della classe GenericDate o di una sua derivata, e
in questo caso eseguo una normale copia, ma se l'oggetto non
e' della sottogerarchia della classe? Con cosa inizializzo
l'oggetto di invocazione? Un costruttore qualcosa dovra' pur
fare..
-- 
Saluti,
 Francesco Cipriani

 fare..
Se il costruttore non e` in grado di completare il suo compito,
come qualsiasi altra funzione, usera` throw per sollevare una
exception.
Meglio, naturalmente, evitare il problema a compile-time, se
possibile, NON permettendo il passaggio se non di oggetti
gia` del tipo giusto.  operator= e copy ctor non li erediti,
quindi nessuno ti _obbliga_ a fargli accettare oggetti il cui
tipo compile-time non ti garba.
Alex

che si vogliono fornire.
Ah beh indubbiamente su questo ho molti molti dubbi, cioe' non so
quando andare su cose un po' "sporche" e quando invece e' meglio
ad esempio creare un'interfaccia astratta e derivare le classi
concrete - sempre che non sia necessario per forza.
Io sono abituato a linguaggi procedurali e a ottimizzare sempre al
massimo tutto, qui mi ritrovo a dover fare certe cose per forza con
alcuni metodi e ho paura di introdurre inefficienze anche dove
magari poi non ce ne sono, il fatto e' che praticamente dovrei sapere
come funziona il C++ a basso livello ma non mi pare proprio il
momento adatto.
-- 
Saluti,
 Francesco Cipriani

 momento adatto.
In questo sono d'accordo (penso...) con Alex nel non considerarla una
buona abitudine; quando ho iniziato a programmare si lavorava su CPU a
16 bit, con clock di qualche MHz e 256 Kb di RAM, il tutto condiviso da
quattro o cinque utenti. Li' si che era necessario ottimizzare tutto. Al
giorno d'oggi, invece, almeno a livello professionale, il codice che
scrivi ha molta piu' importanza come strumento di comunicazione con i
tuoi colleghi che non per dire al computer cosa deve fare, almeno nella
stragrande maggioranza dei casi.
Quindi conta di piu' scrivere in modo chiaro, tale da far capire a chi
legge dove vuoi andare a parare, che non eliminare qualche chiamata a
funzione qua e la. Ad esempio, nell'applicazione su cui sto lavorando
adesso, quando una cifra e' visualizzata in una maschera questa e'
restituita da un metodo di una classe scritta da un mio collega, che
chiama attraverso uno smart-pointer un metodo di una classe mia, che
chiama attraverso un altro smart-pointer (diverso) un metodo di un'altra
classe mia. Questo succede per almeno meta' dei dati presentati, e
nonostante questo tutti i problemi di prestazioni che abbiamo sono
legati all'interrogazione del DB, che nel giro descritto sopra non
c'entra. Per contro questa organizzazione ci permette di stratificare
l;applicazione, separando i compiti tra diverse classi di diversi
sottosistemi.
Ciao,
Nicola

 che non per dire al computer cosa deve fare,
Beh, anche quello ha la sua importanza. :-)
EMG