Praxis Beispiel / Template / Metaprogrammierung
-
Hallo die Runde.
Zum vorausgegangenen Thread Metaprogrammierung wollte ich hier noch mal Beispiele bringen mit Praxisbezug. Um den Post nicht untergehen zu lasen habe ich noch mal ein eigenen Thread eröffnet.
Generell sehe ich in den Templates eine sehr mächtiges Werkzeug das einen viel Arbeit ersparen kann und setze diese so oft wie möglich ein. Fangen wir aber mal an.
Beispiel 1
Ich habe hier viele Objekte die alle die gleiche Basis haben. Dazu gibt es eine Liste in der die Objecte über das Base Objekt in X-beliebiger Reihenfolge gespeichert werden.
class ObjectA : public ObjectBase; class ObjectB : public ObjectBase; class ObjectC : public ObjectBase; usw. typedef std::list<ObjectBase*> tObjectList;Zur Laufzeit Spielt es keine Rolle was und in welcher Reihenfolge diese Objekte gespeichert sind. Nur wenn man das Bearbeitungsfenster schließt, müssen alle gleichen Objekte in eigen Container gespeichert werden. Etwa so:
for(tObjectList::iterator Iter = ObjectList.begin(); Iter != ObjectList.end(); ++Iter) { if(Iter == ObjectA) { AobjectList.push_back(*Iter); } else if (Iter == ObjectB) .... else if(Iter == ObjectC) .... usw. }Das finde ich aber nicht sehr schön, schlecht zum erweitern und macht bei vielen verschiedenen Objekten auch reichlich Arbeit. Auch kann man diese Methode nicht teilen, also wenn man nur ObjectA und ObjectB haben will. Wie hilft uns da ein Template?
Zerteilen wir mal die vorgaben:
a. Wir haben eine Quellen Liste
b. Wir haben eine Ziel Liste
c. Die Ziel Liste kennt den Objekt TypeMachen wir mal ein Template daraus. (Geschwindigkeit spielt an dieser Stelle keine Rolle)
template< typename src_list, typename des_list> void findAndFill(const src_list &SrcList, des_list &DesList) { typedef typename des_list::value_type tZielObject; for(src_list::const_iterator Iter = SrcList.begin(); Iter != SrcList.end(); ++Iter) { if(typeid(tZielObject) == typeid(**Iter)) { DesList.push_back( static_cast<tZielObject*>(*Iter) ); } } }Und Diese Funktion kann man dann einfach nutzen.
tObjectList ObjectListe; tObjectAList ObjectAList; tObjectBList ObjectBList; findAndFill(ObjectListe, ObjectAList); findAndFill(ObjectListe, ObjecteBList);So sieht schon besser aus, einfach zu schreiben und Flexibel oder nicht?

Beispiel 2 – Zweckentfremden von Spezialisierung
Serialisiere – Aha, gibt es Tonnenweise Librarys dafür..... puh, habe mir auch einige angeschaut, aber richtig was Passendes habe ich nicht gefunden. Ich will die Daten in einer XML Ähnlichen Syntax schreiben und lesen. Und so wenig Aufwand beim Programmieren haben. Bin ja Faul. Geben wir mal eine Datensatz vor.
class ObjectFarbe; class ObjectKreis; class ObjectRecheck; class ObjectSet { char *pName; bool Flag; ObjectFarbe Farbe; std::list<Kreis> Kreise; std::list<Rechteck> Rechtecke; };Jedem Objekt geben wir eine Schnittstelle für die Serialisierung und zwar eine für Lesen und Schreiben. Das ist aber noch kein Hexenwerk. Gibt schon librarays die so was machen.
template< typename archiv > archiv& serialisie(archiv &Archiv) { return Archiv.attribute(„Name“, pName) .attribute(„Flag“, Flag) .element(Farbe) .elementContainer(Kreise) .elementContainer(Rechteck); }uns als Datei soll so was rauskommen.
<Set Name=“Name“ Flag=“True“> </Farbe Rot=“1“ Blau=“2“> </Kreis Durchmesser=“1“> </Kreis Durchmesser=“2“> </Rechteck Groesse=“10“> </Set>So weit so gut. Die Attribute werden in den Archiv Classen auch über Template Funktionen Abgebildete sollen aber nicht Bestandteil der Diskussion sein. Sondern die „element“ und „elementContainer“ Funktion.
Frage: Woher wissen die Archiv Classen was für Namen die Element haben? Bei den Farben Element gibt es mehr Möglichkeiten aber wie macht löst man das mit den Containern?
Genau hier scheitern die vielen Librarys.Dazu benutze ich eine Hilfe. Man Ahnt es schon es ist eine Template Struktur.
template< typename element > struct sElementName { static const char * pElementName; };Wie sieht jetzt die Funktion „elementContainer“ aus.
template< typename container > ArchivOut& elementContainer(const container &ObjectList) { typedef sElementName<container::value_type> tElementName; for(container::const_iterator Iter = ObjectList.begin(); Iter != ObjectList.end(); ++Iter) { Out << „<“ << tElementName::pName; Iter-> serialisie(*this); Out << „</> << tElementname::pName; } return *this; }Jetzt kann der Compiler jeden Element einen Eindeutigen Namen zuordnen. Weil für jedes Element Objekt eine Eindeutige Hils-struktur erzeugt wird.
Zum Linken fehlt aber noch was. Denn so steht jetzt eigentlich der Name? Dieses Packe ich in die Source Datei von den Element, das sieht so aus.farbe.cpp template<> sElementName<ObjectFarbe>::pName = „Farbe“; kreis.cpp template<> sElementName<ObjectKreis>::pName = „Kreis“; usw.So das ist meine Lösung der Serialisierung. Hat da jemand Einwände? Immer her damit.
Sind eigentlich solche Beispiel Interessant für euch? Hätte da noch ein Beispiel einer Undo/Redo Strategie.

Lichtlein
-
Zu Beispiel 1:
Das ist in meinen Augen nicht wirklich Metaprogrammierung.
Eine Grenze zu definieren ist zwar schwierig, aber für mich ist
das einfache Template-Programmierung.Außerdem ist das Design-technisch etwas dirty (finde ich).
Wenn ich weiß, dass ich die Typen anders behandeln muss, dann verwalte ich
sie auch separat.Zu Beispiel 2:
Das ist ja quasi ein type trait (oder?)
aber (wie ich denke) sinnvoll angewendet...
-
Zu eins:
Dazu müsste man wohl erst mal ganz genau Definieren was Metaprogrammierung ist und wie man sie erkennen kann. Gibt es nicht Irgendwo eine Liste die man abhacken kann und man dann weiß ob oder ob nicht?
Zu zwei:
Denke da hast Du schon recht das es ein erweitertes "Type trait" ist.Übrigens zum zweiten Beispiel. Da habe ich an Anfang mal ein wenig mit Type Listen Experimentiert. Hat so auch ganz gut Funktioniert.
Sah in etwa so aus:
class ObjectSet { typedef tTypeList< char*, tTypeList< bool, tTypeList< ObjectFarbe...... tObjectSetTypeList; template< typename archiv > archiv& serialisie(archiv &Archiv) { return Archiv.typelist<tObjectSetTypeList>(mContent); } tObjectSetTypeList mContent; };Ist schon ein wenig her und ich weiß nicht wieso ich das dann nicht benutzt habe. Glaube es wahr einfach nur Unüberschichtlich auf den Inhalt zuzugreifen und nicht wirklich gut zu Lesen.
Siehe hier:
ObjectFarbe objectFarbe() const { return field[3]; } void setObjectFarbe(const ObjectFarbe &Farbe) { field[3] = Farbe; }Nett so besonders bei Klassen die mehr als 10 Member Variablen haben.
Lichtlein
-
@Lichtlein
Hättest du eventuell Lust, diesen Post (gern auch in längerer Form mit mehr Beispielen) als Artikel im C++ Magazin zu veröffentlichen? Kurze E-Mail an redaktion at c-plusplus punkt de, falls du Interesse hast

-
Ich halte den Code für Quatsch, sorry.
-
Ich auch mehr oder weniger.
-
Besonders Beispiel 1: RTTI ist doch so ziemlich das genaue Gegenteil von TMP. Für sowas hat man extra Traits entwickelt, um auf solchen Spaß wie typeid und dynamic_cast zu verzichten.
-
Sowas würde ich auch nicht unbedingt als klassische Metaprogrammierung ansehen, obwohl einige TMP-Elemente vorhanden sind. Typische (teilweise Extrem-)Beispiele sind Alexandrescus Modern C++ Design oder einige Bibliotheken aus Boost.
Ad aCTa schrieb:
Für sowas hat man extra Traits entwickelt, um auf solchen Spaß wie typeid und dynamic_cast zu verzichten.
Finde mal die Typ-ID zur Laufzeit mittels Traits heraus. Lichtleins Ansatz geht in die Richtung von "Multimethoden" (dynamisches Dispatching), er könnte allerdings noch generischer und effizienter sein.
-
So wie es aussieht kommt der Code wohl nicht so gut an. Hat sich dann wohl als Artikel erledigt.

Aber noch mal zurück zum Thema.
Beispiel 1
Sollte man es sich nicht so einfach wie möglich machen und die verfügbaren Mittel je nach Anwendungszweck einsetzen? Zu der besagten Liste, diese wird beim Öffnen eines Fenster aus einen Datensatz erzeugt. Beim bearbeiten werden neue Objekte in diese Liste aufgenommen, gelöscht und geändert. Ist dieser Prozess beendet und das Fenster wird geschlossen so werden aus dieser Liste wie gesagt die Verschiedenen Objektetypen geholt und in andere Container gepackt. Also 1 mal wird auf RTTI zurückgegriffen. Was soll daran schlecht sein? "Effizienter" muss es auch nicht sein. Zeit Spielt schlicht keine Rolle.
Noch ein kleiner Hinweis. Die besagte Liste ist eigentlich real ein Komplexer Struktur-Tree, der für die Bearbeitung von max. 1 Millionen Objekten ausgelegt ist. In diesen Fall wird Sie "zweckentfremdet".
Beispiel 2
Wie ich in meinen zweiten Post geschrieben habe, habe ich einmal einen XML Ähnlichen Stream Prototypen geschrieben der auf den Ideen vom "Modernes C++ Design" zurückgreift. (Typelisten) Und wie ich schon beschrieben habe, das ganze ist nicht mehr Leserlich! Man kann den Code einfach nicht Warten. Und nur weil es Cool ist (Ja es ist Cool, wenigstens wenn man es mal Programmiert
)und funktioniert sollte man es nicht einsetzen, wenn man den Code nicht Pflegen kann. Meine Meinung.
Und es würde mich freuen wenn mir Jemand ein XML Ähnlichen Stream zeigt der in der Benutzung so einfach ist wie von mir beschrieben.Lichtlein
-
Zum Beispiel 1:
Was spricht gegen einen klassischen Ansatz ala:#include<iostream> #include<vector> #include<list> #include<deque> class A; class B; class C; class Collections{ public: std::vector<A*> vecA; std::list<B*> vecB; std::deque<C*> vecC; }; class A { public: int i; virtual void assign(Collections& col){ col.vecA.push_back(this); } virtual ~A(){} }; class B : public A { virtual void assign(Collections& col){ col.vecB.push_back(this); } }; class C : public A { virtual void assign(Collections& col){ col.vecC.push_back(this); } }; int main(){ A a1; a1.i = 1; A a2; a2.i = 2; B b1; b1.i = 3; B b2; b2.i = 4; C c1; c1.i = 5; C c2; c2.i = 6; std::vector<A*> vec; vec.push_back(&a1); vec.push_back(&b1); vec.push_back(&c1); vec.push_back(&a2); vec.push_back(&b2); vec.push_back(&c2); Collections col; for(std::vector<A*>::iterator it = vec.begin(); it!=vec.end(); ++it){ (*it)->assign(col); } std::cout << "A:" << std::endl; for(std::vector<A*>::iterator it = col.vecA.begin(); it!=col.vecA.end(); ++it){ std::cout << (*it)->i << std::endl; } std::cout << "B:" << std::endl; for(std::list<B*>::iterator it = col.vecB.begin(); it!=col.vecB.end(); ++it){ std::cout << (*it)->i << std::endl; } std::cout << "C:" << std::endl; for(std::deque<C*>::iterator it = col.vecC.begin(); it!=col.vecC.end(); ++it){ std::cout << (*it)->i << std::endl; } return 0; }A:
1
2
B:
3
4
C:
5
6
-
XSpille schrieb:
Zum Beispiel 1:
Was spricht gegen einen klassischen Ansatz ala:Das ist eine gute Frage, aber noch nicht DIE Frage.
-
volkard schrieb:
XSpille schrieb:
Zum Beispiel 1:
Was spricht gegen einen klassischen Ansatz ala:Das ist eine gute Frage, aber noch nicht DIE Frage.
Was spricht gegen Templates?
-
Templates? schrieb:
volkard schrieb:
XSpille schrieb:
Zum Beispiel 1:
Was spricht gegen einen klassischen Ansatz ala:Das ist eine gute Frage, aber noch nicht DIE Frage.
Was spricht gegen Templates?
Natürlich kannst du auch Abfragen ala
if(typA){ ... } else if(typB){ ... } else if(typC){ ... }machen und was anderes macht er ja quasi nicht.
Er hat genau genommen mehrere Schleifen mit einem if drin,
die er durch Templates auslagert.
Abgesehen davon geht er dadurch noch mehrfach durch die Liste geht.
Aber der Punkt ist (wohl) relativ unerheblich.Um (u. a.) genau solche Konstrukte zu vermeiden, bietet sich ja
Objektorientierung an.
Klar kann man jedes C++ in ein C Programm umwandeln und durch die
Templates vereinfacht er sich quasi das Schreiben eines 'C-Programms'
(zumindest an dieser Stelle).
Zudem sind diese Konstrukte schwerer lesbar als der OO-Ansatz (vielleicht Geschmacksache)Ich glaube, ich würde die Objekte schon während der gesamten Zeit getrennt
verwalten und ein anderes Objekt drum wrappen, damit ich über
alle Elemente iterieren kann.
-
bin nicht so der c++ kenner, aber blähen templates die größe des kompilates nicht extrem auf?
-
@XSpille
Danke.
Mal ein richtiger Vorschlag wie man es besser/anders machen könnte. Ich habe Deine Idee mal ein wenig umgeformt so das man einzelne Werte aus der Liste abfragen kann.template< typename type > struct sSammler { std::list< type > mList; }; struct sBase { typedef void (*tFunc) (); template< typename type > void assign(sSammler< type > &Sammler); virtual tFunc assignRefFun() { return (tFunc) 0; } }; template< typename t > struct sAssign : public sBase { static void refFunc() {}; virtual tFunc assignRefFun() { return &refFunc; } }; template< typename type > void sBase::assign(sSammler< type > &Sammler) { if(&sAssign<type>::refFunc == assignRefFun()) { Sammler.mList.push_back(*static_cast<type*>(this)); } } struct sA : public sAssign<sA> {}; struct sB : public sAssign<sB> {}; struct sC : public sAssign<sC> {}; int main(int argc, char *argv[]) { typedef std::list<sBase*> tBases; tBases J; J.push_back(new sA); J.push_back(new sB); J.push_back(new sA); J.push_back(new sA); J.push_back(new sB); J.push_back(new sC); J.push_back(new sA); cout << J.size() << endl; sSammler<sA> Sa; for(tBases::iterator Iter = J.begin(); Iter != J.end(); ++Iter) { (*Iter)->assign(Sa); } cout << Sa.mList.size() << endl; return EXIT_SUCCESS; }Ok das mit dem Pointer auf eine Funktion als Erkennung ist schon sehr Grenzwertig. Man könnte hier auch mit der TypeToInt Methode Arbeiten. Würde aber wieder mehr schreibaufwand bedeuten. (Mmmm vielleicht sollte ich in mein Projekt alles eine Type Id geben)
Bitte jetzt Steinigen.

Lichtlein
-
no_c0de schrieb:
bin nicht so der c++ kenner, aber blähen templates die größe des kompilates nicht extrem auf?
Kommt drauf an, was man macht. Es kommt natürlich für jede unterschiedliche Templateinstanzierung eine neue Klasse/Funktion hinzu. Jedoch hätte man diese ja in der Regel auch ohne Templates schreiben müssen, um das gleiche zu erreichen.
Es gibt einige Extremfälle von Metaprogrammierung, wo man alle möglichen Programmabläufe durch den Compiler durchrechnen lässt. Dann hat man alle denkbaren Programmergebnisse im Compilat stehen, was natürlich extrem blähen kann (kann aber auch kürzer sein!). Dies sind dann aber wiederum die exotischen Fälle der Metaprogrammierung die in der Praxis niemand benutzt.
-
SeppJ schrieb:
no_c0de schrieb:
bin nicht so der c++ kenner, aber blähen templates die größe des kompilates nicht extrem auf?
Kommt drauf an, was man macht. Es kommt natürlich für jede unterschiedliche Templateinstanzierung eine neue Klasse/Funktion hinzu. Jedoch hätte man diese ja in der Regel auch ohne Templates schreiben müssen, um das gleiche zu erreichen.
Ich würde eher sagen: ohne Templates würde man viele Probleme anders lösen.
Viel Code der durch Templates erzeugt wird ist 1:1 gleich für alle Template-Instanzierungen. Die meisten C++ Programmierer sind aber zu bequem solchen Code aus dem Template in z.B. eine Nicht-Template-Basisklasse rauszuziehen.
An vielen Stellen wird auch nur ein T* rumgereicht, oder vielleicht ein sizeof(T) verwendet, und sonst nix gemacht was für verschiedene T unterschiedlich wäre. Das sind alles Dinge wo man ohne Templates z.B. void-Zeiger verwendet hätte, bzw. Dinge wie sizeof(T) als Parameter mitgegeben/als Member abgespeichert.
Kurz: bei dem was ich als typischen Template-Code kenne, werden Programme definitiv unnötig aufgebläht. Der Begriff "Template Bloat" wurde ja nicht umsonst geprägt.
Man kann Templates allerdings auch so einsetzen, dass der Template Bloat auf ein vernünftiges Mass reduziert wird.Wobei vernünftig natürlich wieder sehr subjektiv und auch vom Programm/Einssatzzweck abhängig. Wenn irgend ein Game/Tool/... was auf modernen Rechnern laufen soll statt 500k statt 200k hat, ist mir das herzlich egal

-
Zum Beispiel arbeiten Boosts Pointer-Container mit
void*stattT*. Ist wahnsinnig praktisch beim Debuggen, wenn man nach den etlichen Indirektionsebenen immer noch nicht auf den konkreten Wert trifft...
Indirektion von
boost::ptr_map(ohne Template-Parameter):boost::ptr_map boost::ptr_map_adapter boost::ptr_container_detail::ptr_map_adapter_base boost::ptr_container_detail::associative_ptr_container boost::ptr_container_detail::reversible_ptr_container std::mapUnd dann steht man vor dem Nichts (genauer gesagt dem Zeiger darauf).