Donnerstag, 2024-11-21, 3:37 PM Willkommen Interessent

Video Game Developers Forum

Suche
Menü
Short News
SNpowered by ShortNews.de
News Verzeichnis
[ Neue Beiträge · Teilnehmer · Forumregeln · Suche · RSS ]
  • Seite 1 von 1
  • 1
Moderator in diesem Forum: Goblin1405  
Eine flexible, aber bequeme Objekt-Factory C++
Goblin1405Datum: Dienstag, 2010-06-08, 7:47 PM | Nachricht # 1
Coder
Gruppe: Administratoren
Nachrichten: 89
Auszeichnungen: 0
Status: Offline
Eine flexible, aber bequeme Objekt-Factory C++

dieser artikel beschreibt, wie eine flexible und einfach zu verwendende object-factory-klasse aussehen kann. Dabei werden "delegates" verwendet, die in den GameDev.de-artikeln "Events und Delegates in C++" und "Events und Delegates in C++ - Reloaded") beschrieben sind.

Gerade habe ich mir überlegt, wie ich am besten (und natürlich möglichst allgemeingültig) Objekte serialisieren, und in einen Stream schreiben, und danach diese Objekte wieder aus dem Stream lesen und deserialisieren kann... in meinem Fall ging es darum, Objekte per Netzwerk zu versenden.

Die erste ordentliche Idee war natürlich:
- jeder Objekttyp bekommt eine ID
- beim serialisieren der Daten wird vor dem serialisieren eines einzelnen
Objekts dessen ID in den Stream geschrieben.

Auf der deserialisierenden Seite muss natürlich das entsprechende Gegenstück sein, das eine Objekt-Typ-ID aus dem Stream liest, ein Objekt vom passenden Typ erzeugt, und mit den folgenden Daten aus dem Stream füllt.

Am besten baut man sich also eine Objekt-Factory, bei der man Typen mit einer ID registrieren kann, und das dann auf Wunsch aus einer solchen ID ein entsprechendes Objekt erzeugt.

Bisher benutze ich für solche Zwecke eine Factory-Klasse, die genau das macht... zur registrierung eines Typs wird eine Register-Methode aufgerufen. Übergeben wird eine ID und ein Funktionspointer. Die übergebene Funktion erzeugt ein Objekt des registrierten Typs und gibt es zurück.

Im Prinzip ist also alles soweit in Ordnung. Allerdings hat das ganze den Haken, dass man für jeden zu registrierenden Typ eine Funktion bereitstellen muss, die ein Objekt dieses Typs erzeugt, und diese beim Registrieren mit übergeben muss.

Das ganze sieht dann so aus:

code

class MyObject1
{
public:
static void *CreateInstance() { return new MyObject1(); }

MyObject1():a(10), b(20)
{
}

protected:
int a, b;

};

class MyObject2
{
public:
static void *CreateInstance() { return new MyObject2(); }

MyObject2():a(20), b(10)
{
}

protected:
int a, b;

};

PIL_DELEGATE(CreateInstanceDelegate, void*, (), ()); // (siehe artikel über delegates)

#define MyObject1_TypeID 1
#define MyObject2_TypeID 2

[...]
Factory::RegisterType(MyObject1_TypeID, CreateInstanceDelegate(MyObject1::CreateInstance));
Factory::RegisterType(MyObject2_TypeID, CreateInstanceDelegate(MyObject2::CreateInstance));
[...]
MyObject1 *object1 = (MyObject1*)Factory::CreateInstance(MyObject1_TypeID);
MyObject2 *object2 = (MyObject2*)Factory::CreateInstance(MyObject2_TypeID);
[...]

Bisher war das für meine Zwecke ok, weil ich nie mehr als eine handvoll Objekttypen gebraucht habe. Aber diesmal sollten es einige mehr sein, und ich hatte keine Lust, für jeden Typ von Hand eine CreateInstance-Methode bereitzustellen, und diese zu registrieren. Nicht nur aus Faulheit, sondern auch weil solche "Krücken" eigentlich relativ unschön sind.

Es wäre natürlich viel eleganter, einen Objekttyp über den folgenden "idealen" Code zu registrieren:

code

Factory::RegisterType(MyObject1, MyObject1_TypeID);

Leider verfügt C++ aber über keine Möglichkeit, einen Objekttyp als "Wert" festzuhalten, um ihn z.B. so wie hier als Parameter an eine Methode zu übergeben. Es gibt zwar die Möglichkeit, Laufzeit-Typinformationen (RTTI) zu verwenden, und man könnte sogar die Type-ID eines Objekttyps an eine Methode übergeben (es handelt sich um einen String). allerdings ist es nicht möglich, anhand dieser Typinformationen eine Objektinstanz zu erzeugen, so wie das z.B. in Java und C# möglich ist.

Die obere Lösung scheint also erstmal auch die einzige Lösung zu sein. Man müsste nur den Compiler dazu bringen, den Code zum erzeugen einer Objektinstanz selber zu generieren.

Solche Arbeit auf den Compiler abwälzen schafft man natürlich am besten mit Hilfe meiner beiden Lieblings-Hilfsmittelchen: Templates und Macros.

Typen können zwar nicht als Parameter an eine Methode übergeben werden, dafür aber sehr wohl als Template-Parameter. Die Folgende Klasse kann eine mit einer Template-Methode eine Instanz des übergebenen Typs erstellen:

code

class Factory
{
public:
template<typename T>static CreateInstance() { return new T(); }
};

[...]
MyObject1 *object1 = (MyObject1*)Factory::CreateInstance<MyObject1>();
[...]

Auf diese Weise generiert der Compiler für jeden beliebigen Typ eine passende CreateInstance-Methode. Es stellt sich nur noch die Frage, wie man die generierten Methoden am besten einer ID zuordnet, so dass nur Anhand dieser ID das zugehörige Objekt erstellt wird...

Die Lösung ist natürlich einfach eine Liste, die pro registriertem Objekttyp eine ID und einen Funktionspointer aufnimmt, der auf die passende generierte CreateInstance-Methode verweist. Statt Funktionspointern habe ich in der folgenden Lösung auf meine Delegate-Klasse (links zu den Artikeln siehe oben) zurückgegriffen:

code

class Factory
{
public:
template<typename T>static void RegisterObjectType(const uint fpObjectID)
{
CreateObjectDelegate createObjectDelegate = CreateObjectDelegate(this, &Factory::CreateObject<T>);
registeredObjectTypes.Add(fpObjectID, createObjectDelegate);
}

static void *CreateInstance(const uint fpObjectID)
{
if (Factory::registeredObjectTypes.ContainsKey(fpObjectID))
{
CreateObjectDelegate createObjectDelegate = Factory::registeredObjectTypes[fpObjectID];
return createObjectDelegate();
}
return NULL;
}
protected:
PIL_DELEGATE(CreateObjectDelegate, void*, (), ());

static pilHashTable<uint, CreateObjectDelegate> registeredObjectTypes;

template<typename T>static void *CreateObject() { return new T(); }
};

/* und natürlich muss eine static-membervariable auch nochmal so in einer .cpp-datei stehen: */
pilHashTable<uint, Factory::CreateObjectDelegate> Factory::registeredObjectTypes;

Hier sieht man die bereits erwähnte CreateInstance-Template-Methode (hier "CreateObject"). zusätzlich ist auch die Methode zur registrierung eines Objekttype ein Template. Darin wird ein Delegate erzeugt, das auf die CreateInstance-Methode für den übergebenen ObjektTyp verweist.

Der Verweis auf das CreateInstance-Template veranlasst den compiler, eine passende Methode zu generieren. Das Delegate lässt sich anschliessend Speichern... in diesem Fall in einer pilHashTable, in der ein uint-Wert (der Objekt-Typ-ID) dem passenden Delegate zugeordnet wird.

Die Methode, die nun von aussen aufgerufen wird, um ein Objekt zu erzeugen ("CreateInstance") benötigt nur noch die Typ-id. Beim Aufruf wird zu dieser ID das passende Delegate gesucht und aufgerufen. Der Rückgabewert des Delegates wird einfach weitergereicht.

Die Verwendung der Factory-Klasse kommt jetzt schon nah an das "Ideal"-Beispiel von oben ran:

code

[...]
Factory::RegisterObjectType<MyObject1>(MyObject1_TypeID);
Factory::RegisterObjectType<MyObject2>(MyObject2_TypeID);

MyObject1 *myObject1 = (MyObject1*)Factory::CreateInstance(MyObject1_TypeID);
MyObject2 *myObject2 = (MyObject2*)Factory::CreateInstance(MyObject2_TypeID);
[...]

Sooo... das war's. Ich hoffe, der ein oder andere kann was damit anfangen.

Quelle: bei Interess bitte klicken

 
  • Seite 1 von 1
  • 1
Suche:

Our Age
Mini Profil


Statistik
Bookmarks
Copyright by GamesArmyLabs © 2024