C++ & Reflection

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Antworten
Benutzeravatar
Jonathan
Establishment
Beiträge: 2380
Registriert: 04.08.2004, 20:06
Kontaktdaten:

C++ & Reflection

Beitrag von Jonathan »

Prinzipiell ist in meinem Spiel jedes Objekt ein Entity, welches verschiedene Komponenten hat. Ein Schwert, das am Boden liegt würde die entsprechenden Item Eigenschaften speichern, ein Charakter hat seine Charakterwerte und so weiter. Jeder Charakter kann auch unterschiedliche Aktionen ausführen (Stehen, Laufen, Angreifen), jede Aktion wird auch wieder durch eine Klasse repräsentiert.

Das System ist soweit ganz hübsch und funktioniert auch gut, man ist auch direkt wahnsinnig flexibel und alles, nur jetzt wird es sehr hässlich, wenn ich das ganze Laden und Speichern will.
Ich würde zum speichern eines Objektes alle Member durchgehen und speichern, einfache (ints) schreibt man direkt komplexere (andere Objekte) haben jeweils eine eigene Speicherfunktion.
Allerdings bekomme ich dadurch natürlich einen riesigen Overhead, eine LaufAktion speichert ja letztendlich nur ein Ziel und die Laufgeschwindigkeit. Wenn ich ja jetzt lade und Speicherfunktionen für schreibe, ist die Klasse direkt doppelt so groß. Noch schlimmer wird es, wenn ich für den Leveleditor alle Parameter aller Objekte ändern können will und dafür GUI Elemente erstellen muss.
Die einzelnen Funktionen sind dann natürlich alle sehr ähnlich, und es ist ermüdend, die immer tippen zu müssen.

Darum versuche ich jetzt irgendwas in Richtung Reflection zu machen. Leider kann C++ das nicht. Und ich habe bisher keine Lösung gefunden, die nicht irgendwie äußerst hässlich wäre.
Hinzu kommt, dass zum Beispiel das laden an sich gar nicht so einfach ist. Wenn man einen Pointer auf ein anderes Entity speichern will, würde man das ja wahrscheinlich durch eine ID ersetzen die man beim Laden wieder durch einen Pointer ersetzen muss, was erst geht, wenn man alle anderen geladen hat.

Ich hab jetzt hin und her überlegt, wie ich das ganze am schönsten machen kann. Man könnte versuchen, alle Klassen von einer Reflectable Klasse erben zu lassen, die Pointer auf alle Member speichert und die man in jedem Konstruktor dann entsprechend befüllt. Funktioniert wohl, aber man speichert dann alles für jedes Objekt, wobei alle Objekte einer Klasse natürlich die selben Member haben.
Wenn man versucht die Infos pro Klasse zu speichern hat man so Probleme wie das Adressen relativ zum this Zeiger nicht mehr Stimmen, wenn man Vererbung benutzt.

Dann gibt es da noch sowas wie Camp. Naja, die Dokumentation ist ziemlich mies, und es scheint so als würde man Member per Getter und Setter ansprechen müssen, und sowas mag ich nicht. Und um an die Member direkt ran zu kommen, muss man deren Typ kennen, der dann nur per enum gespeichert wird und dann muss man wieder switchblöcke machen und so. Insgesamt kann ich noch nicht sehen, was dadran jetzt schöner ist, als an einer selbst programmierten frickel Lösung, und zusätzliche Bibliotheken sind ja erstmal immer schlecht.

Es muss ja vielleicht auch gar kein Reflektion sein, vielleicht findet man ja eine schöne Lösung, wie man zumindest das Laden/Speichern/Editor GUI ELement erstellen und "Methoden bei Skriptsprache registrieren" einfacher und kürzer hinbekommt. Jede Klasse könnte eine Methode haben, die alle Member durchgeht und dafür ein ihr übergebenes Funktionsobjekt aufruft, was man dann passend für den jeweiligen Zweck übergeben könnte, oder so. Wäre auch nicht mehr Tipparbeit, als alle Member bei Camp zu registrieren.



Kurz gesagt: Ich suche eine möglichst schöne Lösung, für etwas, dass mit C++ eigentlich gar nicht geht. Eine Lösung die es mir erspart wirklich viele sehr kurze und primitive Funktionen immer wieder selbst zu tippen.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
anonym
Beiträge: 79
Registriert: 15.07.2009, 07:35
Kontaktdaten:

Re: C++ & Reflection

Beitrag von anonym »

Ich habe die letzte Zeit am gleichen Problem gearbeitet. Camp erfüllt fast alle meine heutigen Anforderungen und hat diese dabei auch noch in ein schönes Design gebracht. Mein Reflectionframework sieht dagegen aus wie ausgekotzt und gehört an einigen Ecken und Enden wohl auch bisschen gestutzt.
Was mich bei Camp aber stört, sind die Unmengen an Templates, die schon bei meiner Lösung die Compilierungszeit massiv in die länge ziehen. Ich bin vor kurzem über gccxml gestolpert, mittels welchem es in einem Prebuild-Step möglich wäre, irgendwie für markierte (Basisklasse?) Klassen und Aufzählungen den Reflection-Code zu generieren. Löst zwar das Problem mit der Compilierungsdauer nicht, eher im Gegenteil, würde aber helfen, sofern das Reflection-Framework dann einmal stabil läuft, viel Arbeit gleichzeitig bei Metaklassendeklaration UND Serialisierung, Gui und Scripting einzusparen.
Ich habe irgendwo einmal auch etwas über Hooks in gcc/g++ während der Compilierung gesehen, finde es allerdings nicht mehr. Wenn die Hooks einigermaßen umfangreich ausfallen, könnte man sich für gcc/g++ den Prebuild-Step sogar schenken.
Hinzu kommt, dass zum Beispiel das laden an sich gar nicht so einfach ist. Wenn man einen Pointer auf ein anderes Entity speichern will, würde man das ja wahrscheinlich durch eine ID ersetzen die man beim Laden wieder durch einen Pointer ersetzen muss, was erst geht, wenn man alle anderen geladen hat.
Die ID ist einfach der bei der Serialisierung eingehende Zeiger, der beim ersten Auftreten dereferenziert und serialisiert wird. Spätere Vorkommen verweisen nur noch auf die Addresse. Beim Deserialisieren wird beim ersten Vorkommen das Objekt erstellt, in einem Wörterbuch gesichert und alle weiteren Vorkommen dann auf dieses Objekt gesetzt. [Edit: Kann in dieser Form wohl durch zyklische Abhängigkeiten knallen. Doch erst alle Zeiger ganz am Ende Dereferenzieren und Deserialisieren] Wiederlich wird es mit Zeigern, wenn nicht alle per Zeiger referenzierte Objekte dynamisch erstellt werden oder Arrays vorkommen. Letztendlich aber alles Definitionsfragen.
Versionsverwaltung des Datenstrukturen sind ein weiteres Problem.
Die nächste Frage ist, ob man nur einfache Strukturen serialisieren möchte, oder auch welche im XML-Stil, auf was ein Entity/Komponenten-System wohl hinausläuft (kein, ein oder mehrere Vorkommen eines Komponententyps möglich). Es müsste für jeden einzelnen Komponententyp ein separater Zeiger im Entity vorkommen, um kein oder ein Vorkommen zu realisieren, anstatt eines einzelnen Containers mit Zeigern auf die Komponenten. Hier muss irgendwie auf Basis des XML-Elements bzw. des Pendants des eigenen Formats auf das Komponentencontainerfeld schließbar sein (Synonyme innerhalb eines Feldes in de rMetaklasse, mehrere auf ein Array verweisende Felder?), der Komponententyp angefordert und davon ein Objekt erstellt und deserialisiert werden und schließlich in den Container eingefügt werden. Auch muss beim XML-Stil überprüft werden, ob alle Elemente und Attribute tatsächlich aus dem Stream "herausgezogen" wurden bzw. keine unbekannten übrig bleiben, sowie bei Attributen Standardwerte setzen, wenn diese nicht angegeben sind. Ob Camp das alles unterstützt, habe ich noch nicht überprüft.
Dynamische Datenstrukturen wie z.B. die Felder eines Constantbuffers können per zur Laufzeit deserialisierter Metaklasse geladen werden. ;)
Reflection ist ein weites, abartiges Feld.

Wenn Interesse an einer über Camp hinausgehenden oder darauf aufbauenden Zusammenarbeit besteht, z.B. die Metaclass-Prebuild-Geschichte oder eines XML-Schema ähnelnden Systems, einfach per PM melden.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2380
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Jonathan »

Hm, also bisher hat mich Camp irgendwie noch nicht überzeugt. Soweit ich das bisher sehen kann, muss man für alle Member eine get/set Funktion haben, um die bei Camp registrieren zu können. Dann muss man beim durchiterieren aller Attribute immer über ein Enum den Typ abfragen, damit man auf die eigentliche Variable zugreifen kann. Das alles ist nicht wesentlich komfortabler, als das was ich so bisher habe, allerdings hat man mit Camp eine externe Lib und durch Templates lange Compilierzeiten:
So werd ich es wohl doch nicht machen, da man die Liste für jedes Objekt einzeln speichert.

Code: Alles auswählen

enum tType{T_NONE, T_INT, T_UINT, T_FLOAT, T_VECTOR3F};

//information about a single class member
struct ReflectableMember
{
	std::string Name;
	void* Value;
	tType Type;

	ReflectableMember(std::string N, void* V, tType T):
	Name(N), Value(V), Type(T) {}
};

//use this macro to register Members in you class
#define REGISTER_MEMBER(Name) this->RegisterMember(#Name, &Name);

//derive your class from this to make it reflectable
class Reflectable
{
public:
	Reflectable(std::string Name): m_Reflectable_Name(Name) {}

	void RegisterMember(std::string Name, int* Value)
	{ m_Reflectable_Members.push_back(ReflectableMember(Name, Value, T_INT)); }

	void RegisterMember(std::string Name, unsigned int* Value)
	{ m_Reflectable_Members.push_back(ReflectableMember(Name, Value, T_UINT)); }

	void RegisterMember(std::string Name, float* Value)
	{ m_Reflectable_Members.push_back(ReflectableMember(Name, Value, T_FLOAT)); }

	void RegisterMember(std::string Name, Vector3f* Value)
	{ m_Reflectable_Members.push_back(ReflectableMember(Name, Value, T_VECTOR3F)); }

	void SaveXml(ticpp::Element *Object);
	void LoadXml(ticpp::Element *Object);
private:
	std::list<ReflectableMember> m_Reflectable_Members;

	std::string m_Reflectable_Name;
};
Es muss ja auch gar nicht ein perfektes Reflectionsystem sein, ich wäre ja schon zufrieden, wenn ich mir die Arbeit irgendwie erleichtern könnte, ohne dass das ganze jetzt wer weiß wie unsauber wird. Im Moment überlege ich, ob nicht sowas funktionieren könnte:

Code: Alles auswählen

class A
{
 int x;
 string name;
 void Reflect(Funktion fun)
 {
   fun("x", T_INT, &x);
   fun("name", T_STRING, &x);
 }
};
Wobei man den Funktionsaufruf ja vielleicht mit Makros noch so vereinfachen kann, dass man einfach fun(x); sagt.

Dann hätte ich natürlich nur eine Schleife, die alle Member durchläuft und könnte nicht per Name auf Member zugreifen. Wobei das eigentlich meistens ausreichen dürfte.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Schrompf
Moderator
Beiträge: 4864
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Schrompf »

Wenn Du's einfach haben willst, dann schreibe wirklich für alle relevanten Klassen die Laden/Speichern-Funktionen manuell. Das ist auf Dauer zwar mühsam zu pflegen, funktioniert aber ohne jeden Zauber und man hat sehr feine Kontrolle darüber, was gespeichert wird. Das ist nützlich und kann für bestimmte Fälle sogar lebensnotwendig sein.

Für den automatischen Weg sieht mir CAMP aber durchaus brauchbar aus. C++ hat halt keine Reflection. Die Metainformationen müssen also vom Programmierer kommen (wie das CAMP macht) oder von einem externen Tool automatisch erzeugt werden. Ich weiß nicht, ob es da eins gibt, aber eigentlich müsste man recht einfach eins schreiben können. Vorausgesetzt, niemand macht irgendwelchen finsteren Tricks mit dem Präprozessor.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8261
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Krishty »

Mit Reflection habe ich keine Erfahrung, weil ich das persönlich als überflüssig ansehe. Aber dennoch neben die Tüte: Kann man nicht den Typ-Kram generell weglassen und stattdessen mit typeid() arbeiten? Denn zumindest Laufzeit-Typinformationen bietet C++ ja an Reflection-Bordmitteln an (noch dazu äußerst effizient).
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Jonathan
Establishment
Beiträge: 2380
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Jonathan »

Krishty hat geschrieben:Denn zumindest Laufzeit-Typinformationen bietet C++ ja an Reflection-Bordmitteln an (noch dazu äußerst effizient).
Hm, ich hab auch mal irgendwo gehört, dass sei gar nicht so sondelrich effizient. Ist natürlich immer die Frage "effizient im Vergleich zu was". Was für Kosten hat man mit sowas konkret?
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Benutzeravatar
Schrompf
Moderator
Beiträge: 4864
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Schrompf »

Google mal nach typeid() - über die kann man nur den Klassennamen herausfinden, nicht deren Member, Basisklassen oder Ableitungen. Damit ist es für Deine Anforderungen nutzlos.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8261
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Krishty »

Jonathan hat geschrieben:Hm, ich hab auch mal irgendwo gehört, dass sei gar nicht so sondelrich effizient. Ist natürlich immer die Frage "effizient im Vergleich zu was". Was für Kosten hat man mit sowas konkret?
Theoretisch keiner, weil es kein C++ ohne RTTI gibt. Auch praktisch – so lange der Typ zur Kompilierzeit feststeht – garkeine. Wenn der Typ polymorph ist (virtuelle Member hat), zwei Zeiger-Indirektionen (der Zeiger auf die Typinformation wird im virtual-function-table gespeichert) pro Abfrage und Anzahl_Typen × sizeof(type_info) Bytes zusätzliche read-only-Information in der Exe (also quasi garnichts bzw. das gleiche wie virtuelle Funktionen generell kosten). Je nachdem, wie generell man es haben möchte, könnte es aber sein, dass man mehr Objekte polymorph machen muss als man momentan hat.
Schrompf hat geschrieben:über die kann man nur den Klassennamen herausfinden, nicht deren Member, Basisklassen oder Ableitungen. Damit ist es für Deine Anforderungen nutzlos.
Ich meinte aus dem Code herausgelesen zu haben, dass er auch das braucht – und sicherer, als von Hand T_INT, T_STRING usw. einzugeben, ist es auf jeden Fall. Mit Member-iterieren und sowas hast du aber recht … das muss er schon selber machen; dafür bringt C++ keine Bordmittel mit.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Schrompf
Moderator
Beiträge: 4864
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Schrompf »

Krishty hat geschrieben:
Schrompf hat geschrieben:über die kann man nur den Klassennamen herausfinden, nicht deren Member, Basisklassen oder Ableitungen. Damit ist es für Deine Anforderungen nutzlos.
Ich meinte aus dem Code herausgelesen zu haben, dass er auch das braucht – und sicherer, als von Hand T_INT, T_STRING usw. einzugeben, ist es auf jeden Fall. Mit Member-iterieren und sowas hast du aber recht … das muss er schon selber machen; dafür bringt C++ keine Bordmittel mit.
Japp, den Namen findet man damit raus, wenn auch nicht Compiler-unabhängig. Nur leider nicht mehr. Und das ärgert mich. Man hätte doch wenn dann gleich ein ordentliches typeinfo erfinden können, dass auch Funktionen, Membervars und wasweißichnoch auflistet. Oder zumindest die Basisklassen. Oder wasweißich. Grmpf.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Jonathan
Establishment
Beiträge: 2380
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: C++ & Reflection

Beitrag von Jonathan »

Ich habe beschlossen, das ganze System anders anzugehen. Ich hatte eigentlich vor ein recht flexibles aber arg komplexes Speicherformat zu benutzen, aber ich hab jetzt ein gute Idee, wie ich es vereinfachen kann und damit kann ich jetzt z.B. boost::Serialization benutzen und alles ist gut.
Lieber dumm fragen, als dumm bleiben!
https://jonathank.de/games/
Antworten