Newsgroup: it.comp.lang.c++
Titolo: I template non preservano l'ereditarietà?

Salve a tutti.
Mi spiego meglio rispetto al subject.
Ho istanziato un template di una classe contenitore, per esempio una
lista, particolarizzandola per il contenimento di una certa classe che
chiamo Padre. Successivamente le passo una classe figlia (aggiungo alla
lista un'istanza della figlia).
Al momento di recuperare questa instanza passata (prelevando l'istanza
subito dopo dalla lista), essa mi ritorna della classe padre.
Risolvo il problema se invece di particolarizzare il template per la
classe Padre, lo particolarizzo per Padre*, con la conseguenza che mi
devo occupare all'esterno della lista della costruzione e della
distruzione degli elementi.
E' il modo giusto di procedere o sto sbagliando?
Grazie,
Marcello.
P.S.: Allego un piccolo programmino per chiarire il concetto da leggere
solo se non fossi riuscito a spiegarmi bene.
#include <stdio.h>
class Padre {
  public:
    virtual void Print() { printf("Classe Padre.\n"); };
};
class Figlio: public Padre {
  public:
    virtual void Print() { printf("Classe Figlio.\n"); };
};
template <class Tipo> class Container {
    Tipo inf;
  public:
    void Set(Tipo T) { inf = T; };
    Tipo Get() { return inf; };
};
int main() {
  Figlio F;
  Container<Padre> Cont;
  Cont.Set(F);
  (Cont.Get()).Print();  //Invece di chiamare Print() del Figlio, chiama
Print() del Padre.
};

 E' il modo giusto di procedere o sto sbagliando?
Dipende da cosa vuoi fare. Un modo per evitare il memory management e'
usare il pattern letter-envelope.
In pratica hai una classe base, chiamata Envelope, che contiente un
Envelope*
(tipico trucco: Envelope isA e hasA Envelope contemporaneamente...).
Poi derivi quante Letter vuoi da Envelope.
Tutte le volte che costruisci o distruggi un Envelope ti occupi della
allocazione o distruzione dell'Envelope*.
Nel costruttore di Envelope prendi come argomento il tipo di Letter che
vuoi mettere dentro all'Envelope...
Se adesso templatizzi i tuoi container a Envelope, i container si occupano
della
copia o distruzione dei tuoi Envelope, e tu non ti devi piu' preocucpare di
nulla.
In realta' non e' cosi' semplice, ci sono un po' di trucchi a cui stare
attenti: ti rimando
al libro di Bruce "Thinking in C++" nella sezione dei costruttori virtuali e
ti suggerisco di guardarti questo pattern li'.
Il punto debole di questo pattern e' che crea una fortissima dipendenza
architetturale tra Envelope e le varie Letter. In particolare se decidi di
informare Envelope con, per esempio, una stringa di quale tipo di Letter
deve contenere, Envelope deve conoscere tutte le Letter che potra'
contenere: se aggiungi una Letter, devi modificare il codice di Envelope. Se
invece decidi di passare al costruttore di Envelope un puntatore a Envelope
che punta ad una tua generica Letter, allora sei
punto daccapo con il problema del memory management.
Bruce Eckel (non si scrive cosi', ma non mi ricordo mai la grafia del
cognome)
affronta il problema in TIC++. Secondo lui un uso frequente dei container
e' quello di essere contenitori di oggetti di tipo diverso accumunati da una
sola interfaccia.  In questo pattern il contenitore e' un contenitore di
puntatori
alla classe interfaccia, e purtroppo l'utente deve preoccuparsi di tutte le
problematiche legate al memory management. Bruce osserva anche che
questo e' una debolezza delle STL.


 Print() del Padre.
la stessa chiamata, con la nuova definizione di Container, funziona
invece come desideri.  E non devi preoccuparti di distruggere i
contenuti, ci pensa lui.
Usando una funzione-membro a sua volta templatizzata si puo`
fare di meglio -- immagina di aggiungere al corpo di Container,
subito dopo la Set:
    template<class Derivata>
    void SetAny(const Derivata& d) { Set(new Derivata(d)); };
provvede allora lei a chiamare internamente il giusto copy ctor
(in pratica, un "clone dei poveri"...!-), e nel codice cliente,
invece di
    Cont.Set(new Figlio(F)); // copy ctor giusto ed esplicito
puoi scrivere
    Cont.SetAny(F);    // copy ctor implicito, sempre giusto
Attenzione pero` che, a differenza della clone, il polimorfismo
non e` pienamente garantito: SetAny usa il tipo _compile
time_ dell'argomento (genericita`), NON il suo tipo _run time_
(polimorfismo a pieno titolo).
Comunque, in certi casi puo` bastare, sopratutto se
quest'ultima limitazione viene tenuta ben presente!-)
Alex

 NON USATELA, e sarete piu` contenti":-).
Per eredita' di implementazione si intende: avere una classe Padre da cui
si possono creare oggetti, e avere una classe Figlio derivata da Padre da cui
si
possono creare oggetti?

 questo e' una debolezza delle STL.
"Debolezza" che sparisce istantaneamente usando le Envelope qui
presentate.
Naturalmente, anche il clonatore o qualche versione del reference
count possono essere implementate per genericita`, consentendo
massima comodita` di riuso del codice.  Ad esempio, se non
occorrono costruttori non di default, puo` bastare addirittura:
template <class T>
class cloner: public T {
public:
    virtual T* clone() const
        return new cloner(*this);
    }
};
e in ciascun punto della factory dove "normalmente" si farebbe,
per una certa classe concreta X,
    return new X;
si scrivera` invece:
    return new cloner<X>;
Questo richiede che le classi in gioco abbiano distruttore virtuale
(presumibilmente questo e` scontato, se no tutti questi sforzi
per usarle polimorficamente sono malintesi:-) e un copy constructor
accessibile (almeno protected), oltre appunto al default ctor che
deve essere accessibile dagli appositi punti della factory.
[NB: questo puo` dare problemi in implementazioni di C++ che
*ancora* non supportino correttamente i return-type covarianti,
che furono a suo tempo introdotti proprio per facilitare il lavoro
di funzioni virtuali di tipo "clone"; fra i compilatori C++ attuali e
almeno semi-decenti, conosco solo Microsoft VC++ che abbia
ancora problemi con questo costrutto...].
Come esercizio per il lettore, si propone di scrivere una analoga
implementazione generica di reference-counter sotto ipotesi
piu` o meno analoghe.
Alex

 si possono creare oggetti?
Il C++ permette tutte le "sfumature intermedie" fra piena concretezza
(quella che hai appena definito) e piena astrazione (quella che Lakos
chiama "classe-protocollo", e corrisponde esattamente alla semantica
di una "interface" in Java o COM).
Il problema dello slicing nel passaggio per valore si pone solo nei casi
in cui l'eredita` verte su classi "totalmente concrete".
L'eredita` di (parziale) implementazione puo` riguardare anche casi
di eredita` da classi "parzialmente" concrete (astratte, cioe` non tali
da poter essere istanziate, ma contenenti ANCHE qualche dato); si
hanno allora problemi, minori, ma pur sempre problemi.
Il consenso cui nel mio post mi riferivo verte sopratutto sui casi piu`
chiari, quelli di eredita` fra classi pienamente concrete; man mano
che gradualmente ci si sposta dalle classi concrete a quelle totalmente
astratte, i problemi calano in proporzione; di conseguenza cala anche,
altrettanto gradualmente, il "consenso" in questione.
E` inoltre forse meglio che richiami ancora una volta, visto che su
questo gruppo non lo avro` scritto piu` di 5,000 volte, 10,000 al
massimo, che tutto questo NON si applica a quelle interessanti
sinergie fra genericita` ed eredita` di implementazione che
consentono l'esistenza della "Pattern Without A Name" (PWAN)
in tutte le sue varianti -- templates che ereditano da un proprio
parametro ovvero sono fatti per fungere da classe-base per uno
dei propri parametri -- il miglior mezzo in C++ di ottenere riuso
di codice in molte, molte situazioni.  Non si applica inoltre ad
un'altra "strana interazione" fra genericita` ed eredita`, che e`
l'uso di marker-classes.
Alex

 distruzione degli elementi.
Non vorrei dire stupidaggini ma non puoi utilizzare dei Padre&?
EMG

 NON USATELA, e sarete piu` contenti":-).
Ma questo vale solo per il C++ o anche per altri linguaggi progettati
diversamente?
Per esempio questo svantaggio si ha anche quando si usano linguaggi con
l'adozione del modello ad oggetti per riferimento?
Io non mi ritengo un esperto, ma uso l'ereditarietą in maniera
abbastanza spinta da anni... e sono contentissimo. :)
Salutoni,
Marcello.


Infatti le ho dette. Ma perché scrivo queste cavolate?...
Scusate.
EMG

 Marcello.


Certo. e' piu' flessibile, ma non e' detto che sia un requisito...
Se l'unico modo giusto per mettere e togliere gli oggetti polimorfi da un
contenitore della STL fosse metterli dentro una envelope templatizzata,
allora questo dovrebbe essere obbligatorio e trasparente all'utente. Se
l'unico modo giusto di creare un oggetto fosse la Factory, allora non dovrebbe
essere permesso di usare un costruttore.
Se l'unico modo giusto di trattare oggetti con membri allocati dinamicamente
fosse
implementare un copy constructor, un distruttore virtuale e un operatore di
assegnamento (unicamente implementato per proteggere da a=a...) allora...
Uh oh.
Capisco.
:)

Infatti le ho dette. Ma perché scrivo queste cavolate?...
Una eccessiva abbronzatura da monitor?   ;-)
Paolo
--------------------------------------------------
  Paolo Marotta
  Computer Science, University of Pisa
  home:   
http://www.cli.di.unipi.it/~marotta
--------------------------------------------------

 abbastanza spinta da anni... e sono contentissimo. :)
Immagino tu intenda eredita` di implementazione?  In che
linguaggi?  Con che tipo di caratteristiche di riusabilita`?
Alex

 :)
Buon per te...!-)
Alex

 In quale linguaggio?
Delphi.
Ho approcciato per necessità al C++, ma vengo dal mondo Pascal, che mi
ha dato grossa soddisfazione.
Trovandomi a dover lavorare in C++ ho dovuto impararne il modello di
implementazione dell'OOP e ne ho apprezzato alcune caratteristiche.
Principalmente i template che nel Pascal non esistono, ed in una prima
fase anche il fatto che gli oggetti non fossero intrinsecamente dei
puntatori automaticamente dereferenziati. Quest'ultimo aspetto si è però
poi mostrato un'arma a doppio taglio... da cui il mio post.
Mi mancano invece i class-reference e le proprietà.
Salutoni,
Marcello.

 linguaggi?  Con che tipo di caratteristiche di riusabilita`?
Delphi.
Sģ, intendo l'ereditarietą di implementazione. Nel mio piccolo ritengo
che la riusabilitą di codice non sia ciecamente ripudiabile in cambio di
un modello teoricamente perfetto.
Mi rendo conto che questa mia considerazione si basa sulla stesso motivo
ispiratore che ha fatto nascere i linguaggi RAD; ma bisogna pur
accettare che un ambiente di sviluppo alla fine serve per realizzare
pogrammi che comunque devono finire per modellare un ambiente reale (con
tutti i suoi problemi ed imperfezioni).
Da qui la necessitą di scendere a compromessi che fanno rabbrividire i
puristi.
Salutoni,
Marcello.

 puristi.
Sono un ingegnere, non un purista, e il mio disgusto nei
confronti dell'eredita` da classi concrete nasce anzitutto dai
problemi che questa pratica porta, appunto, in pratica.
Ma, come sopra accennavo, i problemi concettuali, proprio
nella modellazione della realta` per mezzo delle mie classi,
non sono da meno.
Cosa puo` significare, in un mio sistema ad oggetti, avere
una classe "Uccello" ma anche una classe "Tortora" che
eredita da "Uccello"?  Significa: sto modellando a livello
delle _specie_; le mie classi concrete (supponendo che
Tortora lo sia) rappresentano delle specie.  Ma allora non
ha senso che una classe concreta, istanziabile, mi stia
invece a rappresentare _l'intero genere_.  Non esiste, nella
realta`, una istanza di "Uccello" che non sia anche una
istanza di *una specifica specie di Uccello*.
Quindi, se Tortora eredita da Uccello, bisogna, per integrita`
concettuale del modello, che Uccello non sia istanziabile.
"La mappa non e` il territorio", certo (quindi e` del tutto
accettabile avere una classe "AltriUccelliCheNonSoBene"
che modella "tutte le altre specie di uccelli non modellate
da una loro specifica classe", ad esempio!), ma neppure
la mappa presenta una scala 1:1,000,000 per certe cose
e 1:100,000 per altre -- altrimenti, e` una mappa molto
mal congegnata, con grosse distorsioni di tanti aspetti
importanti.
Alex

 Una eccessiva abbronzatura da monitor?   ;-)
Può essere. :-)))
EMG