Design-Problem mit Templates
-
Moin. Ich bin gerade schwer beschäftigt, mir ein kleines GUI-Framework zu basteln. Ich komme zu dem Schluss: ich HASSE HASSE HASSE templates...
Die Basis-Daten:
Das ganze GUI-System ist in sich geschlossen. Nach außen sind nur zwei Klassen bekannt: die Basis-Klasse**Ctl
von der alle Steuerelemente abgeleitet werden und die ZentralklasseUIManager
die auch als Klassenfabrik fungiert.
Die Steuerelement-Eigenschaften werden nicht als Member-Variablen deklariert sondern sind in Objekten vom Typtemplate <class T> CtlProperty : public CtlProperty0
gekapselt.
Alle Eigenschaften werden in einer Liste abgelegt (list<CtlProperty0*> Ctl::m_lstProperties
**).
Von außen Zugriff hat man über die template-Methodetemplate <class T> Ctl::Get () const
bzw. entsprechende Set-Methode.
Für z.B.**Get()
**gibts jetzt folgende Varianten:1.: downcast
template <class T> T Ctl::Get (CtlPropertyType type) const { CtlProperty<T>* pTmp = dynamic_cast<CtlProperty<T>*> (m_lstProperties.find (type)); if (NULL == pTmp) throw Error ("template-Parameter falsch. ätschbätsch"); else return pTmp->GetValue(); }
Vorteil: Typsicher
Nachteil: Zu restriktiv. ein als int initialisiertes Property kann man sich nicht z.B. als long geben lassen, man muss immer genau den richtigen Datentyp für jede Eigenschaft im Auge behalten.
a) Fehleranfällig
b) an einer Stelle ein echtes Design-Problem (=UITheme-Klasse komplett umbauen. *grrrr*)Variante 2: void*
Ich spar mir jetz das Listing...
Vorteil: alle Nachteile aus Variante1 lösen sich in Wohlgefallen auf.
Nachteil: ZEIGER AUF VOID - cast... super-hyper-Typ-unsicher.Variante 3: stringstream
Mein bisheriges Konzept.
Ein kompliziertes Gewurste mit Speichern als String und hin und her casten mit std::stringstream plus noch weiteren von CtlProperty0 abgeleiteten Klassen für Zeiger und Objekte.
Typunsicher, viel zu zeitaufwändig.Ich brauche ein Mittelding. sowas muss funktionieren:
Ctl* pTxt = m_pUIManager->CreateCtl (CtlTypes::TextBox); pTxt->Set<int> (CtlPropTypes::Value, 3); long l = pTxt->Get<long> (CtlPropTypes::Value);
Und das sollte einen Compiler-Fehler generieren (oder irgendwas um zur Laufzeit darauf reagieren zu können)
pTxt->Set<Ctl*> (CtlPropTypes::Parent, pWnd); int i = pTxt->Get<int> (CtlPropTypes::Parent);
Kann mir jemand mit nem Gedankenblitz aushelfen?
-
Hier würde mich auch eine lösung interessieren. Hab ein ähnliches Problem unter .Net. Gelöst hab ichs aktuell mit object Objekten. Welches wohl einen void pointer gleichkommen würde. Ich kann jetzt nicht genau sagen wie sich die void ptr beim Cast verhalten. Im .net sollte dadurch das meist abgedeckt sein. Falls die Klassen nicht übereinstimmen geht der Cast mit einer Exception schief. Sofern sowas ähnliches auch mit void ptr machbar ist. Wäre das gar nicht mehr so typunsicher wie man es sich vorstellt. Obwohl die Lösung immer noch nicht wirklich das wahre ist.
-
Mit dem Entscheid, Basisklassenzeiger und
enum
als Objekt-ID zu verwenden, verlierst du statische Typsicherheit und somit ist eine Prüfung zur Compilezeit unmöglich.Für die Property-Klasse fände ich es zweckmässig, ähnliche Eigenschaften zusammenzupacken, z.B. in eine Klasse
IntegerProperty
für Ganzzahlen oderStringProperty
für Strings. Damit legt man sich nicht auf einen einzelnen Typ fest und gewinnt Flexibilität. Man könnte in die Klasse implizite Konvertierungsoperatoren zu den jeweiligen Typen (z.B.long
,int
,short
respektiveconst char*
,std::string
) einbauen, vielleicht sogar als Funktionstemplates.Deine Variante 1 ist im Prinzip nicht schlecht, man könnte sie folgendermassen ausbauen:
class Property { /* ... */ }; class IntegralProperty : public Property { /* ... */ }; class StringProperty : public Property { /* ... */ }; template <class PropertyClass> PropertyClass& Ctl::Get(PropertyType identifier) const { Property* basePtr = m_lstProperties.find(identifier); return dynamic_cast<PropertyClass&>(*basePtr); } int main() { Ctl* myControl = /* ... */; long value = myControl->Get<IntegralProperty>(CtlPropTypes::Value); }
Hierbei wirft
dynamic_cast
bei Fehlschlag einestd::bad_cast
-Exception. Da der dynamische Downcast bei einem korrekten Programm nicht fehlschlägt, aber dennoch relativ viel Rechenzeit verbraucht, würde ich stattdessenstatic_cast
im Release- unddynamic_cast
im Debug-Modus einsetzen (oder gleichboost::polymorphic_downcast
verwenden).Schön wäre natürlich, wenn das
Get()
-Funktionstemplate gleich erkennen könnte, zu welcher Klasse eine Property-ID wieCtlPropTypes::Value
gehört. Auch das ist möglich, mit einer relativ einfachen Typ-Metafunktion. Das Makro dient der Übersichtlichkeit:#define PROPERTY_ID_TO_CLASS(ID, CLASS) \ template <> \ struct IdToProperty<ID> \ { \ typedef CLASS Type; \ }; template <typename T> struct IdToProperty; PROPERTY_ID_TO_CLASS(CtlPropTypes::Value, IntegralProperty) PROPERTY_ID_TO_CLASS(CtlPropTypes::Parent, IntegralProperty) PROPERTY_ID_TO_CLASS(CtlPropTypes::Text, StringProperty)
Angewandt:
template <CtlPropTypes::Type Identifier> // CtlPropTypes::Type ist dein enum typename IdToProperty<Identifier>::Type& Ctl::Get() const { Property* basePtr = m_lstProperties.find(Identifier); return dynamic_cast<typename IdToProperty<Identifier>::Type&>(*basePtr); } int main() { Ctl* myControl = /* ... */; long value = myControl->Get<CtlPropTypes::Value>(); }
Möglicherweise sind noch ein paar Fehler drin, ich hoffe aber, das Prinzip ist verständlich. Ansonsten frage einfach nach. Fedaykin, auf C# wird das aufgrund fehlender Metaprogrammierung wohl nicht 1:1 übertragbar sein, aber da kann man sicher mit Reflection etwas Ähnliches hinbekommen.
-
Ich komme zu dem Schluss: ich HASSE HASSE HASSE templates...
Warum benutzt du sie dann? Andere GUI-Bibliotheken kommen auch ohne aus.