PDA

Visualizza la versione completa : [C++] Ereditarietà, Superclasse ecc


Neptune
31-10-2010, 14:28
Salve a tutti,
stavo seguendo in linea teoria la creazioni di superclassi e classi che ne ereditano i metodi per poi estenderli e volevo sapere in c++ in termini di linee di codice come si faceva.

Mettiamo caso che ho una classe "Matrice", che contiene metodi come Leggi e Scrivi, poi voglio fare un'altra classe che si chiama "Natural Merge Sort" che avrà bisogno dei metodi Leggi e Scrivi e quindi andrà ad estendere la classe "Matrice". Questo in termini di codice come si traduce? Come potrà la classe "Natural Merge Sort", sempre in termini di codice, utilizzare il metodo "Leggi"?

Perchè nel corso di algoritmi e strutture dati c'è stato detto che non ci venga di implementare nella stessa classe della struttura dati (esempio matrice) funzioni di più alto livello (ad esempio l'algoritmo di ordinamento). E quindi ho pensato che bisogna fare una superclasse che implementi le operazioni base della struttura dati ed una classe figlio, che ne eredita i metodi, che poi implementerà funzioni come l'ordinamento.

Sbaglio qualcosa in questo ragionamento?

Vi ringrazio in anticipo,
Neptune.

MItaly
31-10-2010, 15:35
Una possibilità è fare sì che la superclasse che contiene l'algoritmo di ordinamento erediti da una classe completamente virtuale (o classe astratta virtuale (http://www.parashift.com/c++-faq-lite/abcs.html)*) che dichiara i metodi per accedere agli elementi, così che qualunque container che erediti dalla classe dell'algoritmo di ordinamento sia obbligato ad implementare questi metodi virtuali puri per poter essere istanziabile.

* in linguaggi che le supportano sarebbe un'interfaccia - e in C++ è bene che segua le regole delle interfacce: niente campi di dati in modo da evitare casini con l'ereditarietà multipla (http://www.parashift.com/c++-faq-lite/multiple-inheritance.html#faq-25.8), potenzialmente solo funzioni virtuali pure (http://www.parashift.com/c++-faq-lite/abcs.html#faq-22.4).


template <typename T>
class RandomAccessibleContainer
{
public:
virtual T & Element(size_t Index) = 0;
virtual size_t Count () = 0;
};

template <typename T>
class QuickSortable : RandomAccessibleContainer<T>
{
public:
void Sort(); // implementata nel .cpp
};

template <typename T>
class Vector : QuickSortable<T>
{
public:
// ...
virtual T & Element(size_t Index); // implementata nel .cpp; l'avere questa funzione effettivamente definita consente a Sort() di funzionare correttamente
virtual size_t Count (); // idem con patate
}

Naturalmente per avere uno schema più flessibile (per container che ad esempio non consentono un accesso casuale, come le liste) un bisogna implementare un po' più di astrazione:


// Classe base per tutti i container in cui si può accedere a qualunque elemento direttamente
template <typename T>
class RandomAccessibleContainer
{
public:
virtual T & Element(size_t Index) = 0;
virtual size_t Count () = 0;
};

// Classe base per tutti i container in cui si può accedere agli elementi solo sequenzialmente
template <typename T>
class SequentiallyAccessibleContainer
{
public:
virtual bool NextElement() = 0;
virtual bool PreviousElement() = 0;
virtual T & Element() = 0;
virtual size_t Count () = 0;
};

// Classe base per tutte le classi che implementano un sort
class Sortable
{
public:
virtual void Sort() = 0;
};

template <typename T>
class QuickSortable : Sortable, RandomAccessibleContainer<T> // Il quicksort richiede un accesso casuale agli elementi
{
public:
virtual void Sort(); // questo è effettivamente implementato nel .cpp
};

template <typename T>
class StrangeSortable : Sortable, SequentiallyAccessibleContainer<T> // Supponiamo che al nostro StrangeSort sia più efficiente del quicksort se si può solo andare avanti e indietro nel container
{
public:
virtual void Sort(); // questo è effettivamente implementato nel .cpp
};

template <typename T>
class Vector : QuickSortable<T>
{
public:
// ...
virtual T & Element(size_t Index); // implementata nel .cpp; l'avere questa funzione effettivamente definita consente a Sort() di funzionare correttamente
virtual size_t Count (); // idem con patate
};

template <typename T>
class DoublyLinkedList : StrangeSortable<T>
{
public:
// ...
virtual bool NextElement(); // tutte implementate nel .cpp, consentono a Sort() di funzionare
virtual bool PreviousElement();
virtual T & Element();
virtual size_t Count ();
};

D'altra parte, in un'accezione meno stretta di OOP si possono vedere gli algoritmi come funzioni libere che operano su container che hanno determinate caratteristiche. In tal caso, eviteremmo in blocco tutte le classi "***Sortable" e i container erediterebbero direttamente dalla classe-base che descrive il tipo di accesso che si può avere ai loro elementi. Si potrebbe implementare quindi una funzione Sort con gli overload adeguati ad ogni tipo di ***AccessibleContainer.


// SequentiallyAccessibleContainer e RandomAccessibleContainer come nell'esempio precedente

template <typename T>
class Vector : RandomAccessibleContainer<T>
{
public:
// ...
virtual T & Element(size_t Index); // implementata nel .cpp
virtual size_t Count (); // idem con patate
};

template <typename T>
class DoublyLinkedList : SequentiallyAccessibleContainer<T>
{
public:
// ...
virtual bool NextElement(); // tutte implementate nel .cpp
virtual bool PreviousElement();
virtual T & Element();
virtual size_t Count ();
};

template <T>
void Sort(RandomAccessibleContainer<T> & Cont)
{
// ...
}

template <T>
void Sort(SequentiallyAccessibleContainer<T> & Cont)
{
// ...
}

Spingendosi ancora più lontani dall'OOP classico, c'è l'approccio di generic programming della STL: gli algoritmi sono tutti funzioni libere, e i container non ereditano da classi basi virtuali che ne descrivono l'accesso; piuttosto, forniscono come metodo preferenziale di accesso agli elementi (almeno per gli algoritmi) degli iteratori, ossia degli oggetti che possono essere incrementati/decrementati/confrontati/... per spostarsi nel container.
L'"interfaccia comune" sfruttata dagli algoritmi per operare sugli iteratori sono gli operatori che essi ridefiniscono (++ passa all'elemento successivo, -- al precedente, ==/!= valutano se i due operatori sono nella stessa posizione nel container, + va un tot di elementi oltre, * (inteso come operatore unario di dereferenziazione) ottiene il valore relativo alla posizione nella sequenza, ...); si possono vedere come generalizzazione dei puntatori su container di qualunque genere.

Naturalmente non tutti i container consentono le stesse operazioni: in un vettore si può saltare qua e là senza problemi, in una doubly-linked list ci si può muovere solo avanti e indietro.
La questione è gestita fornendo agli iteratori degli operatori solo per le operazioni che sanno compiere: un iteratore relativo ad una lista avrà ++ e --, ma non + o -, né (in linea di massima) > o <. Al contrario, un iteratore relativo ad un vector avrà più o meno tutti gli operatori possibili (++, --, +, -, >, <, ==, !=, ...), dato che si possono implementare in maniera molto efficiente tramite operazioni sui puntatori.

Avendo iteratori non legati ad un tipo particolare, ma che possono essere di qualunque tipo purché implementino gli operatori adeguati, non è possibile sfruttare l'overloading "classico" per scegliere quale particolare versione dell'algoritmo richiamare: l'unico appiglio che si ha per scegliere qual'è la versione giusta è capire quali operatori implementa l'iteratore; per questo interviene la SFINAE (http://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error), che viene sfruttata per far scegliere al compilatore la versione dell'algoritmo più adeguata all'iteratore passato.

L'approccio STL, rispetto ai metodi citati sopra, ha sicuramente come vantaggio una maggiore efficienza, dato che tutte le chiamate vengono risolte a compile-time e vengono evitate in blocco le chiamate virtuali; inoltre si riesce ad effettuare spesso l'inlining di buona parte delle funzioni, che in genere sono molto brevi e guadagnano abbastanza da questo tipo di ottimizzazione.
Per contro, lo stile STL è meno comprensibile da chi ha un background di OOP "classica", e l'uso degli iteratori può dare origine a codice verboso e non troppo leggibile. D'altra parte essi consentono una flessibilità unica, sia per chi implementa un container nuovo (che, fornendo iteratori, può essere immediatamente gestito da tutti gli algoritmi già esistenti) sia per chi implementa nuovi algoritmi, che si possono applicare a qualunque container già esistente, a patto che fornisca iteratori che implementano gli operatori necessari all'algoritmo.

Per inciso, è possibile implementare un approccio ad iteratori andando di sole classi virtuali senza tutti i trucchi di template e SFINAE, basterebbe fare sì che tutti gli iteratori forniti dai container ereditassero da una classe virtuale che ne definisce gli operatori, spostando cioè sugli iteratori quello che rappresentavano le ***AccessibleContainer sui container negli esempi precedenti.
D'altra parte questo approccio annullerebbe parte dei benefici in termini di ottimizzazione (gli iteratori avrebbero chiamate virtuali a palate) e richiederebbe che tutti fossero d'accordo sul tipo di classe base da impiegare per ogni iteratore, cosa che in C++ di rado accade. :D

Inoltre, dato che in C++ i tipi primitivi non sono oggetti e non possono ereditare da nulla, un normale puntatore relativo ad un array (che di suo implementa gli operatori necessari per praticamente tutti gli algoritmi: ++, --, +, -, >, <, !=, ==, *, ...) non potrebbe essere impiegato in un algoritmo che richiederebbe che gli iteratori passati ereditino da una determinata classe base; al contrario, i puntatori possono essere impiegati senza problemi negli algoritmi template usati nella STL, dato che l'unica cosa che importa non è il loro tipo, quanto gli operatori che implementano.

Neptune
31-10-2010, 18:52
Originariamente inviato da MItaly
[..]

Scusami se non sono riuscito a seguire fino in fondo il thread, probabilmente quando andrò più affondo con il corso di Algoritmi e Strutture Dati capirò meglio ogni parola.

Ad ogni modo, volendola fare facile l'esempio delle operazioni di lettura e scrittura come superclasse ed una classe figlio che fa l'ordinamento, in modo da poter astrarre dalle modalità di accesso alla struttura dati, potrebbe essere qualcosa del genere?



class StrutturaDati {
public:
...
int leggi(..);
int scrivi(..);
...
};

class Ordinamento : public StrutturaDati {
public:
...
void ordina();
...
};


Cosi facendo nel metodo ordina potrei utilizzare i metodi di leggi e scrivi per poter effettuare l'ordinamento? Ma pensandoci meglio, se leggi e scrivi sono metodi pubblici in realtà qualsiasi classe potrebbe utilizzarli senza problemi e senza aver bisogno di classe padre e figlio, o sbaglio?

Facendo questo tipo di ereditarietà potrei andare a modificare direttamente le variabili private della superclasse ovvero di "StrutturaDati" da una funzione di Ordinamento?

So che più in la nel corso si parlerà di template, ed altri mille metodi per effetturare questa cosa, ma ora come ora per concretizzare le poche nozioni teoriche introdotte volevo trovare già un metodo per il c++, ovviamente quello che concettualmente e praticamente sia il più semplice.

Poi so anche di ereditarietà multipla ed altre e mille "cosucce" che sicuramente ripeto mi saranno più chiare proseguendo il corso.

Ti ringrazio come al solito per la pazienza nel rispondermi :fagiano:

MItaly
31-10-2010, 21:55
Originariamente inviato da Neptune
Cosi facendo nel metodo ordina potrei utilizzare i metodi di leggi e scrivi per poter effettuare l'ordinamento? Ma pensandoci meglio, se leggi e scrivi sono metodi pubblici in realtà qualsiasi classe potrebbe utilizzarli senza problemi e senza aver bisogno di classe padre e figlio, o sbaglio?

Infatti, a questo punto meglio avere gli algoritmi come funzioni libere.
Inoltre un approccio di questo genere è assolutamente non flessibile, e alla fin della fiera non astrae un bel niente: se inizi ad avere più algoritmi e ne devi usare più di uno sulla stessa classe come speri di cavartela? Non puoi creare una classe di tipo Ordinamento, perché in tal caso ti manca l'altro algoritmo, e viceversa. Se vuoi usare un approccio ad ereditarietà a container e algoritmi, devono essere i container ad ereditare dagli algoritmi, i quali tuttavia devono poter sapere "in anticipo" (tramite ereditarietà da una classe base astratta) quali saranno i metodi che le classi derivate impiegheranno per farle accedere ai dati.


Facendo questo tipo di ereditarietà potrei andare a modificare direttamente le variabili private della superclasse ovvero di "StrutturaDati" da una funzione di Ordinamento?
No, solo le variabili protected, ma anche se potessi in generale non è buona pratica. E comunque non ha senso una struttura ad ereditarietà di quel genere.


So che più in la nel corso si parlerà di template, ed altri mille metodi per effetturare questa cosa, ma ora come ora per concretizzare le poche nozioni teoriche introdotte volevo trovare già un metodo per il c++, ovviamente quello che concettualmente e praticamente sia il più semplice.

Poi so anche di ereditarietà multipla ed altre e mille "cosucce" che sicuramente ripeto mi saranno più chiare proseguendo il corso.
Difficile parlare di un design flessibile di container e algoritmi senza parlare né di funzioni virtuali né di template... almeno uno dei due concetti serve... :stordita:


Ti ringrazio come al solito per la pazienza nel rispondermi :fagiano:
Non c'è di che, scrivere quel post ha chiarito le idee anche a me. :)

Neptune
01-11-2010, 14:28
Originariamente inviato da MItaly
Se vuoi usare un approccio ad ereditarietà a container e algoritmi, devono essere i container ad ereditare dagli algoritmi, i quali tuttavia devono poter sapere "in anticipo" (tramite ereditarietà da una classe base astratta) quali saranno i metodi che le classi derivate impiegheranno per farle accedere ai dati.

Effettivamente avevo sentito parlare di questo appproccio "inverso".

Cioè la superclasse sarà "ordinamento", che però deve avere almeno in maniera "virtuale" delle classi di leggi e scrivi, che poi andranno ad essere implementate materialmente nelle classi figlio e varieranno da struttura dati a struttura dati, giusto?

Cioè io dichiaro ordinamento come superclasse che usa poi un'altra classe per leggere e scrivere.

Ma con la classe virtuale, ai fini dell'astrazione, come posso fare a dire in un secondo momento che quel leggi e scrivi si riferiscono ad una lista anzichè ad un array anzichè a non so che altro?

Se scrivessi superclasse Ordinamento che usa i metodi Matrice::Leggi e Matrice::Scrivi, poi dopo, dovrò a forza usare una matrice. Se volessi invece usare dei metodi "generali" e poi solo in un secondo momento dichiararli come "lettura di matrici" o di "liste"? O addirittura scegliere se si avvera una determinata condizione usa una lista, altrimenti una matrice?

Per capire un pò meglio all'atto pratico la cosa, sempre che non ti è troppo dispersivo in termini di tempo, ti andrebbe di dichiararmi in maniera pratica una classe ordinamento, che usa i metodi di una classe virtuale leggi e scrivi, e poi dichiarati in un secondo momento questa classe virtuale in modo da poter decidere se usare una lista o un array? Giusto perchè magari con un pò di codice alla mano su cui discutere magari mi riesce meglio capire :fagiano:

Neptune
01-11-2010, 14:35
Stiamo parlando di qualcosa del genere?



//*********************** PRIMO PASSO
class Shape {
public:
...
virtual Shape* clone() const = 0; // The Virtual (Copy) Constructor
...
};
//********************* SECONDO PASSO
class Circle : public Shape {
public:
...
virtual Circle* clone() const;
...
};
//********************* TERZP PASSO

Circle* Circle::clone() const
{
return new Circle(*this);
}


Dimmi se ho capito bene, in shape dichiara il metodo virtuale "clone()", poi crea la classe Circle come figlia di Shape e dichiara il metodo virtuale clone() che poi sviluppa alla fine?

Non riesco però a capire tutto quel giro di puntatori. Riusciresti a produrmi un esempio un minimo più semplice? :fagiano: (magari quello dell'ordinamento lo è, ripeto sempre se non ti ruba troppo tempo)

MItaly
01-11-2010, 17:00
Originariamente inviato da Neptune
Effettivamente avevo sentito parlare di questo appproccio "inverso".

Cioè la superclasse sarà "ordinamento", che però deve avere almeno in maniera "virtuale" delle classi di leggi e scrivi,
I metodi leggi e scrivi. In linea di massima comunque sarebbe bene che i metodi leggi e scrivi siano in una classe ancora più su nella gerarchia, in maniera tale che se la classe contenitore vuole ereditare da due classi-algoritmo non abbia casini perché entrambe le classi vogliono i "loro" leggi e scrivi. L'idea, un po' più espansa per includere anche un altro algoritmo, sarebbe


+--------------------+ +------------------------+
| RandomAccessible | < indica un container | SequentiallyAccessible | < indica un container
+--------------------+ ad accesso casuale +------------------------+ ad accesso sequenziale
| virtual leggi = 0 | | virtual gonext = 0 |
| virtual scrivi = 0 | | virtual goprev = 0 |
+--------------------+ | virtual cur = 0 |
|| indica un container +------------------------+ Indica un container in cui
|| ordinabile || \\ si può effettuare una ricerca
|| V || \\ V
|| +--------------------+ || \\____________ +--------------------+
|| | Sortable | || \------------\ | Searchable |
|| +--------------------+ || \\ +--------------------+
|| | virtual ordina = 0 | || \\ | virtual cerca = 0 |
|| +--------------------+ || supponiamo che lo \\ +--------------------+
|| //\\ || strangesort sia \\ ||
|| ________________// \\____________________ || molto efficiente con \\ ||
|| /----------------/ \--------------------\ || container sequenziali \\ ||
|| // \\ || V \\ ||
+--------------------+ +-----------------------------+ +---------------------+
| QuickSortable | <-- il quicksort | StrangeSortable | | LinearSearchable |
+--------------------+ necessita di un +-----------------------------+ +---------------------+
| ordina | accesso casuale | ordina (usa i metodi | | cerca (usa i metodi |
| (usa leggi/scrivi) | | di SequentiallyAccessible | | di Seq.Accessible |
+--------------------+ | per accedere agli elementi) | +---------------------+
|| +-----------------------------+ // A
|| _________________________________________________| |________________________________ // \\per una ricerca lineare
|| /-------------------------------------------------||--- -----------------------------/ basta poter leggere gli
+-----------------------+ || // elementi sequenzialmente
| Vector | +-----------------------------+
+-----------------------+ < un vettore è | DoublyLinkedList | < su una lista si possono applicare
| leggi (implementata) | sia ad accesso +-----------------------------+ algoritmi che operano ad accesso
| scrivi (implementata) | casuale che | gonext (implementata) | sequenziale
| gonext (implementata) | sequenziale | goprev (implementata) |
| goprev (implementata) | | cur (implementata) |
| cur (implementata) | +-----------------------------+
+-----------------------+

Sia su Vector che su DoublyLinkedList si possono richiamare ordina e cerca; il
primo metodo (virtuale in Sortable) è implementato in maniera differente: su
Vector richiama QuickSortable::ordina, che usa Vector::leggi e Vector::scrivi
tramite i metodi virtuali di RandomAccessible, mentre su DoublyLinkedList usa
StrangeSortable::ordina, che usa i metodi virtuali di SequentiallyAccessible
implementati in DoublyLinkedList).
Al contrario, cerca è sempre lo stesso (dichiarato come virtuale in Searchable,
implementato in LinearSearchable), che usa i metodi virtuali di Sequentially-
Accessible. Questi tuttavia sono implementati in maniera differente in
DoublyLinkedList (dove useranno i soliti sistemi della lista) e in Vector (dove
saranno un semplice incremento/decremento di un puntatore).

(perdona l'ASCII-art, ma non ho molto di meglio sottomano :stordita: )

che poi andranno ad essere implementate materialmente nelle classi figlio e varieranno da struttura dati a struttura dati, giusto?
Esatto.


Cioè io dichiaro ordinamento come superclasse che usa poi un'altra classe per leggere e scrivere.
Esatto, la classe derivata definisce effettivamente i metodi leggi e scrivi (il trucco magico della superclasse virtuale consente di far usare agli algoritmi metodi che al loro livello della gerarchia non sono ancora definiti).

Ma con la classe virtuale, ai fini dell'astrazione, come posso fare a dire in un secondo momento che quel leggi e scrivi si riferiscono ad una lista anzichè ad un array anzichè a non so che altro?
Sì, anche se in realtà una lista difficilmente avrà dei leggi e scrivi uguali ad un vettore anche come interfaccia, dato che in una lista ci si può muovere avanti e indietro, mentre saltare ad un determinato indice è molto dispendioso, per cui, come nell'esempio sopra, tendenzialmente ci sarà un'altra superclasse con i metodi che sono "tipici" della lista (spostati avanti, indietro, elemento corrente) che verranno sfruttati dagli algoritmi ottimizzati in maniera particolare per questa situazione.
Nota che se ad un algoritmo bastano requisiti minimi (potersi muovere semplicemente avanti, come capita per una ricerca lineare) nulla impedisce che si possano applicare anche a container più "evoluti": le operazioni di base di una lista si possono implementare anche su un vettore, per cui se noi abbiamo una superclasse di ricerca lineare che eredita dalla superclasse che definisce gonext, goprev e cur, nulla ci impedisce di far ereditare la classe Vector da questa, implementando gonext, goprev e cur in maniera adeguata per i vettori (cosa estremamente semplice). Così, come nell'esempio, possiamo applicare sia QuickSortable che LinearSearchable ad un Vector.


Se scrivessi superclasse Ordinamento che usa i metodi Matrice::Leggi e Matrice::Scrivi, poi dopo, dovrò a forza usare una matrice. Se volessi invece usare dei metodi "generali" e poi solo in un secondo momento dichiararli come "lettura di matrici" o di "liste"? O addirittura scegliere se si avvera una determinata condizione usa una lista, altrimenti una matrice?
Guarda l'esempio.


Per capire un pò meglio all'atto pratico la cosa, sempre che non ti è troppo dispersivo in termini di tempo, ti andrebbe di dichiararmi in maniera pratica una classe ordinamento, che usa i metodi di una classe virtuale leggi e scrivi, e poi dichiarati in un secondo momento questa classe virtuale in modo da poter decidere se usare una lista o un array? Giusto perchè magari con un pò di codice alla mano su cui discutere magari mi riesce meglio capire :fagiano:
È più o meno quello che ho fatto nell'esempio, senza scrivere codice ma delineandone la struttura, così come ho fatto nel post ancora prima nei vari esempi. Tieni conto che non è una grande idea usare un algoritmo su container non adatti ad esso, ad esempio applicare una ricerca binaria su una lista è assolutamente senza senso (saltare avanti e indietro "a balzelloni" su una lista non è efficiente), mentre, come detto, algoritmi che hanno requisiti "minimi" (ricerca lineare) si possono applicare più o meno dovunque.

MItaly
01-11-2010, 17:15
Originariamente inviato da Neptune
[...]

Quello è un esempio convoluto, perché serve ad aggirare una limitazione del C++. Vedila in maniera più semplice: una classe astratta Shape che implementa un metodo Draw. Dato che è una forma "in generale", non si sa cosa bisogna disegnare, semplicemente si sa che le classi che sono delle Shape dovranno poter essere disegnate.
Dunque,


class Shape
{
public:
// ...
virtual void Draw(Image & Img) = 0; // supponiamo che Image sia un oggetto su cui si può disegnare
};

class Square
{
public:
// ...
virtual void Draw(Image & Img); // <-- non c'è il "= 0", significa che qui la implementiamo per davvero
};

Square::Draw(Image & Img)
{
// qui si disegna effettivamente il quadrato
// ...
}

class Triangle
{
public:
// ...
virtual void Draw(Image & Img); // <-- non c'è il "= 0", significa che qui la implementiamo per davvero
};

Triangle::Draw(Image & Img)
{
// qui si disegna effettivamente il triangolo
// ...
}


In questa maniera si possono trattare gli oggetti Shape "in generale": immagina ad esempio una funzione che accetta un parametro di tipo Shape &: può essere qualunque cosa, l'unica cosa che importa alla funzione è di poter usare l'interfaccia "generale" delle Shape (ad esempio il metodo Draw) definita a livello di superclasse. L'implementazione effettiva poi varia da una classe derivata all'altra, ma la funzione non necessita di saperne nulla, chiama Draw su un oggetto che sa essere un generico oggetto derivato da Shape, e magicamente viene chiamata la Draw giusta.

MItaly
02-11-2010, 20:20
A proposito del modello degli iteratori, puoi dare un'occhiata rapida qui (http://www.cplusplus.com/reference/std/iterator/) per avere una panoramica dei tipi di iteratori e dei loro requisiti.

Neptune
02-11-2010, 20:22
Originariamente inviato da MItaly
Quello è un esempio convoluto, perché serve ad aggirare una limitazione del C++. Vedila in maniera più semplice: una classe astratta Shape che implementa un metodo Draw. Dato che è una forma "in generale", non si sa cosa bisogna disegnare, semplicemente si sa che le classi che sono delle Shape dovranno poter essere disegnate.
Dunque,


class Shape
{
public:
// ...
virtual void Draw(Image & Img) = 0; // supponiamo che Image sia un oggetto su cui si può disegnare
};

class Square
{
public:
// ...
virtual void Draw(Image & Img); // <-- non c'è il "= 0", significa che qui la implementiamo per davvero
};

Square::Draw(Image & Img)
{
// qui si disegna effettivamente il quadrato
// ...
}

class Triangle
{
public:
// ...
virtual void Draw(Image & Img); // <-- non c'è il "= 0", significa che qui la implementiamo per davvero
};

Triangle::Draw(Image & Img)
{
// qui si disegna effettivamente il triangolo
// ...
}


In questa maniera si possono trattare gli oggetti Shape "in generale": immagina ad esempio una funzione che accetta un parametro di tipo Shape &: può essere qualunque cosa, l'unica cosa che importa alla funzione è di poter usare l'interfaccia "generale" delle Shape (ad esempio il metodo Draw) definita a livello di superclasse. L'implementazione effettiva poi varia da una classe derivata all'altra, ma la funzione non necessita di saperne nulla, chiama Draw su un oggetto che sa essere un generico oggetto derivato da Shape, e magicamente viene chiamata la Draw giusta.

Non capisco qui l'utilità dela classe "shape". Ciop alla fine dei conti noi non andremo sempre a dichiare oggetti specifici ovvero di tipo triangle o square andando ad utilizzare i loro metodi? E allora se non dichiaremo mai nulla di tipo "shape" a che serve dichiararlo come superclasse?

Loading