Giochiamo col polimorfismo in C++

Data

by Quake2

 

27/07/2004

UIC's Home Page

Published by Quequero


"That is not dead which can eternal lie / And with strange aeons even death may die." H.P. Lovecraft

Devo ammettere che quake si e' proprio spremuto, ha fatto ben due tute in un giorno ;ppp, ah se avete una minima conoscenza di STL vi tornera' utile nel tute.

"But as, in ethics, evil is a consequence of good, so, in fact, out of joy is sorrow born" E.A. Poe

....

Home page se presente: http://pmode.cjb.net
E-mail: [email protected]
Nick: Quake2^AM, UIN: 51184823, AzzurraNet: #gameprog-ita, #crack-it, #asm, #pmode, #c#, #programmazione, #beos

....

Difficoltà

( )NewBies ( )Intermedio (x)Avanzato ( )Master

 

In questo tutorial parlerò di una tecnica carina per poter istanziare delle classi a runtime in base al loro nome, senza sapere in anticipo cosa dobbiamo istanziare :).


Giochiamo col polimorfismo in C++
(ovvero: facciamo fare al nostro programma cose impensabili :))
Written by Quake2

Introduzione

In questo tutorial un po' inusuale, vedremo una tecnica interessante per poter istanziare in base al loro nome delle classi, ma a che può servire? Beh la prima cosa che mi viene in mente è un linguaggio di scripting, o cose simili, insomma situazioni in cui uno non può sapere cosa deve fare, e deve affidarsi a qualche tecnica. Ad esempio, una tecnica consiste nell'esportare da una dll due funzioni, una per creare un oggetto e una per distruggerlo, in questo modo è molto più semplice creare classi in base al loro nome, basta infatti usare una nomenclatura standard per le funzioni e cambiargli solo l'ultima parte in base alla classe, poi con una funzione che wrappa LoadLibrary e GetProcAddress si fa tutto facilmente. Ma se noi non volessimo usare una dll? Che ci facciamo GetProcAddress su un eseguibile? Si potremmo (infondo un eseguibile può avere una export table), ma sarebbe bruttissimo, e poi porterebbe qualche complicazione, mentre il modo che vi mostrerò può essere usato sia per un eseguibile sia per una dll, e non porta particolari complicazioni

Tools usati

Un compilatore C++ abbastanza recente (quindi il turbo c 1.0 non va bene), il codice l'ho sviluppato e provato sotto Visual C, ma sono sicuro che funziona anche su GCC, l'unico problema è che non so come si attiva il RTTI su GCC, ma se lo usate dovreste saperlo :).

URL o FTP del programma


Quale programma? :)

Notizie sul programma

Ancora? Ma siete duri eh :)

Essay

Bene, siccome ho parlato molto di quello che vedremo nell'introduzione, qua iniziamo subito col codice, per prima create un nuovo progetto col Visual C, fate Win32 Console Application, create un empty project, e attivate il RTTI, andando su Project->Properties->C/C++->Language mettendo Enable Run-Time Type Info a yes, fatto ciò siamo pronti per partire (ovviamente questo tutorial presuppone una conoscenza del C++ e del polimorfismo in generale, perché parlare del polimorfismo richiederebbe un tutorial a parte (o meglio un libro) e va fuori da ciò di cui voglio parlare :)):

#include <iostream>
#include <map>
#include <typeinfo.h>

using namespace std;

//questa macro serve per creare una funzione che ritorna un puntatore all'oggetto baseclass (che in questo caso è un interfaccia),
//dal quale deriva classname la funzione avrà nome function

#define DYNAMIC_LOAD(baseclass, classname, function) \
    inline baseclass *function() \
    { \
        return new classname; \
    } \

//questa macro invece serve per generare il puntatore a funzione (la funzione definita da DYNAMIC_LOAD), baseclass è l'oggetto/interfaccia
//dalla quale dovranno derivare le classi che si desidera istanziare in questo modo

#define LOAD_FUNCTION(baseclass, function) \
    typedef baseclass *(*function)(); \

//classe base da cui devono derivare tutte le classi che si desidera istanziare in questo modo, da notare che è un interfaccia
//(costruttore protetto, ovvero la classe non è
istanziabile direttamente)
//il mio consiglio è di usare sempre un interfaccia tipo quella che ho fatto io per questo genere di cose
//una cosa IMPORTANTISSIMA: la classe/interfaccia base deve contenere ALMENO UNA funzione virtuale per far si che typeid(*this) ritorni
//le informazioni giuste, questo perché una classe/interfaccia senza funzioni virtuali (o virtuali pure) non ha senso per il concetto
//stesso di polimorfismo, quindi typeid vi ritorna sempre il tipo della classe base, indipendentemente da cosa gli passate
//mentre se la classe contiene almeno una funzione virtuale, typeid(this) ritorna il tipo della classe base (che però noi conosciamo)
//mentre typeid(*this) ritornerà il tipo della classe derivata, che invece non conosciamo a runtime
class Object
{
    protected:
        Object() {}

    public:
        virtual ~Object() {}
        const type_info& Type() const { return typeid(*this); }
        bool IsEqual(const Object& obj) { return (Type() == obj.Type()); }
};

//la prima classe che istanzieremo dinamicamente
class ObjSon1 : public Object
{
    public:
        ObjSon1() { cout << "ObjSon1 creato." << endl; }
        ~ObjSon1() { cout << "ObjSon1 distrutto." << endl; }
};

//e dopo la prima c'è la seconda :)
class ObjSon2 : public Object
{
    public:
        ObjSon2() { cout << "ObjSon2 creato." << endl; }
        ~ObjSon2() { cout << "ObjSon2 distrutto." << endl; }
};

//generiamo le varie funzioni necessarie
LOAD_FUNCTION(Object, LoadClass)
DYNAMIC_LOAD(Object, ObjSon1, LoadSon1)
DYNAMIC_LOAD(Object, ObjSon2, LoadSon2)

//una map che conterrà l'associazione nomelcasse->funzione, questo permetterà di poter istanziare una classe in base al suo nome, ovviamente
//la funzione è un puntatore a funzione che ritorna il tipo generico Object, che poi verrà trasformato nel tipo che ci serve, potenza del polimorfismo :)
map<string, LoadClass> clsMap;

int main()
{
    string choise;
    Object *obj = NULL;
      
    //aggiungiamo gli elementi alla mappa, non siamo costretti ad usare lo stesso nome per la classe, ma possiamo usarne uno qualsiasi
    //l'importante è che il nome della funzione sia lo stesso che abbiamo generato con la macro
    clsMap["ObjSon1"] = LoadSon1;
    clsMap["ObjSon2"] = LoadSon2;

    cout << "Che classe vuoi istanziare? ";
    cin >> choise;

    //dopo aver chiesto che classe creare, la creiamo! :), come vedete, non dobbiamo fare ne if ne niente, diamo alla map solo il nome, ovviamente ci
    //sarebbe da fare un po' di error checking, ma questo è solo un esempio, le () servono perché come ricorderete, il secondo elemento della map era un
    //puntatore a funzione, quindi facendo clsMap[chiave] effettivamente ritorniamo un puntatore a funzione
    obj = clsMap[choise]();

    //per convincervi ulteriormente che questo metodo funziona, faccio stampare il tipo dell'oggetto creato, e magicamente vederete che
    //per ObjSon1 sarà "class ObjSon1" per ObjSon2 sarà "class ObjSon2"
    cout << "Ho creato qualcosa! :) E precisamente: " << obj->Type().name() << endl;
    cout << "E ora lo distruggo!" << endl;

    delete obj;
    return 0;
}

Ok compilate il file ed eseguite, scegliere la classe che volete creare, e magicamente verrà creata :)
Ovviamente le possibilità non finiscono qua, ansi il metodo utilizzato da questo tutorial è molto statico (basta vedere tutte le macro), ad esempio avreste potuto che so utilizzare un file
dal quale leggete le classi che volete istanziare e magari in che dll stanno queste classi, la dll esposterà delle funzioni ben definite, e voi potete farvi una classe che permette di caricare
in maniera dinamica le classi sia da una dll, sia dal vostro stesso exe, in modo automatico, se utilizzate lo stesso metodo.
Oppure potete farvi tipo un sistema per gestire delle configurazioni, ma voi non sapete il numero di configurazioni da gestire, in questo modo vi fate un array di Object *, e poi create gli oggetti a seconda di come stanno scritti in questo file, insomam le possibilità sono infinite, basta un po' di fantasia :).

Beh ho finito, so che è un tutorial microscopico, ma preferisco le cose piccole ma buone (non pensate male) :)

                                                                                                                                                          Quake2

Note finali


Beh come al solito spero vi sia piaciuto il tutorial, e come sempre vi invito a mandare le minacce per email.
Stavolta non saluto nessuno perché ho salutato tutti nel mio precedente tutorial fatto lo stesso giorno di questo, quindi andatevi a vedere quello per i saluti :)
Ciao!
                                                                                                                                                           Quake2

Disclaimer


Non mi assumo nessuna responsabilità in caso il codice qui riportato arrechi danni permamenti al vostro compilatore, ovvero se inizia a bestemmiare, prendervi in giro, rifituarsi di compilare i vostri file, modificarvi il codice per dispetto, crearvi i bug per farvi impazzire, non è colpa mia. Ricordate che i compilatori non vanno maltrattati, ma vanno trattati con rispetto, non fategli compilare milioni di righe di codice al giorno, imparate a compilarvi i vostri programmi a mano, è ora di smetterla con questo sfruttamento.
Ricordate che il codice non va copiato o rubato, ma va scritto, quindi che lo leggete a fare sto tutorial? :)

Noi programmiamo al solo scopo di perdere tempo e peggiorare la nostra vista.