Zur Laufzeit Liste von unterstützten Schnittstellen eines Objektes erfahren



  • Hallo Leute,

    in meiner GUI-Anwendung hatte ich vor längerer Zeit einmal für kontextsensitive Dinge ein System entwickelt, mit dem man für die aktuell selektierten "Entities" eine Liste von gemeinsam unterstützten Schnittstellen in Erfahrung bringen kann (um Buttons zu enablen, Properties anzuzeigen, Kontextmenüs anzubieten und was so alles abhängig von den Entities sein kann). Mein Ziel war, dass ich nicht eine Mega-Basisklasse habe und 95% davon in den meisten Fällen mit irgendwelchen Stubbies implementiere - allgemein um unnötige Kopplung zu vermeiden. Das funktionierte auf Bassis des Visitor-Patterns, aber etwas unkonventionell: Die besuchte Klasse versucht der Einfachheit halber per dynamic_cast den Visitor auf Interface-spezifische Visitor-Typen zu casten, wenn das klappt, dann wird der Double-Dispatch im gewöhnlichen Typsystem vollendet. Mag wohl seine Nachteile haben, aber ein Vorteil ist, dass es immerhin funktioniert (bis jetzt). Die Idee war, dass die Vererbungs-Hierarchie bei den Visitoren genau 1 ist, wohingegen bei den Entities durchaus wüste Typzusammenstellungen vorliegen können, ansonsten hätte ich ja auch gleich per dynamic_cast auf den Entities arbeiten können. Um die Last bei den Klassen gering zu halten, habe ich ein kleines Konstrukt von Template-Klassen ersonnen, die Gebrauch von einer virtuellen Basisklasse machen. Und das ist jetzt das Problem.
    Im Laufe der letzten Wochen habe ich eine andere Komponente (weit unterhalb der GUI angesiedelt) entwickelt, die entsprechend meiner persönlichen Entwicklung heftigere Template-Konstrukte, Ducktyping usw. verwendet und mit der ich auch sehr zufrieden bin. Jetzt kommt das Problem:
    Während der Entwicklung letzterer Komponente kam ich nicht auf die Idee auch einmal zu überprüfen, wie das mit virtuellen Basisklassen ist (weil ich die auch praktisch nie verwende und folglich kaum daran denke).
    Jetzt ist die "Kacke aber am Dampfen", weil Visual C++ 2013 die Kombination von beidem mit einem internen Compilerfehler quittiert (siehe auch https://connect.microsoft.com/VisualStudio/feedback/details/809367/visual-c-internal-compiler-error-with-virtual-base-class).

    Folglich habe ich jetzt das Problem entweder die neue Komponente einzuschrotten oder ein neues Schnittstellen-System zu basteln, das ohne virtuelle Basisklassen zurechtkommt (Ich kann NICHT den Compiler wechseln 😞 ). Letzteres wäre mir eigentlich lieber, weil das sowieso schon alt ist und vielleicht einmal neue Ansätze vertragen könnte. Daher meine Frage: Kennt ihr Artikel, die sich mit der Implementierung eines solchen Schnittstellen-Systems beschäftigen, vorzugsweise auf einem Stand der Zeit, als templates schon in C++ angekommen sind und vielleicht einige Hacks unnötig gemacht haben? Im Prinzip hätte ich gerne eine Basisklasse, die es ermöglich Schnittstellen per "QueryInterface" in Erfahrung zu bringen, und möglichst wenig Aufwand, diese in den letztlichen Klassen zu implementieren und zu "publizieren".
    Mit etwas Lesestoff bei der Hand würde ich mir erhoffen, die Experimentier-Phase abzukürzen, weil mir dieses Problem gerade echt ungelegen daherkommt, wenn ich einmal untertreiben darf.

    Meine Google-Suche verlief eben recht ergebnislos. Vielleicht sind "C++ dynamic interfaces" und Kombinationen davon erweitert mit anderen Szene-Wörtern aber auch nicht die richtigen Begriffe. Haben sich für mein Anliegen irgendwelche Begriffe eingesiedelt?

    Viele Grüße,
    deci



  • Hrmmm, ich habe einmal ein bissl rumgefrickelt und könnte solch ein Muster zum Laufen bringen:

    class Base1 :
    	public ex::Reflects< Base1,
    		ex::Public<>,
    		ex::Implements< Iface1 >
    	>
    {};
    
    class Base2 :
    	public ex::Reflects< Base2, 
    		ex::Public<>, 
    		ex::Implements< Iface2 >
    	>
    {};
    
    class MyClassDerived :
    	public ex::Reflects< MyClassDerived,
    		ex::Public<Base1, Base2>,
    		ex::Implements< >
    	>
    {
    };
    

    Das ganze auf Kosten eines Zeigers pro Basisklasse, die keine weiteren Basisklassen hat, verhält sich auch während der Konstruktionphase eigentlich wie ein vtable-Zeiger.

    Aber das ist ja praktisch genauso "intrusiv", wie wenn ich halt in jeder Klasse eine Reflection-Methode überschreibe und dort von Hand die Schnittstellen rausfische, beziehungsweise an die Basisklassen weiterleite. Der Template-Code lässt weniger "Code-Fehler" zu, aber ist insgesamt wahrscheinlich ebenso fehleranfällig, wie das immer per Hand auszuschreiben, hrmmm...



  • Mal davon ab, dass man so nicht mehr direkt an die Konstruktoren der Basisklassen herankommt, oder aber zweimal die Basis-Listen ausschreiben muss...



  • Wie dynamisch darf das Ganze sein? Grundsätzlich besteht eine Möglichkeit darin, den einzelnen Klassen eindeutige Typ-Flags zuzuweisen, und dann mit bitweisen Operatoren Mengenoperationen abzubilden. Wenn du das clever machst, kannst du das sogar zur Laufzeit automatisieren.

    Hier eine Beispielimplementierung mit vollständig automatisierter Typregistrierung, ohne Codeduplizierung und ohne Makros. Nimmt allerdings an, dass Basis- und abgeleitete Klassen strikt getrennt sind und dass nicht mehr als 8*sizeof(unsigned int) Basisklassen vorkommen.

    // Verteilt fortlaufende IDs
    unsigned int NextID()
    {
    	static unsigned int id = 1;
    	unsigned int old = id;
    	id <<= 1;
    	return old;
    }
    
    // CRTP-Klasse, von der Basisklassen erben
    template <typename Self>
    struct Base
    {
    	virtual unsigned int ClassID() const
    	{
    		static unsigned int id = NextID();
    		return id;
    	}
    };
    
    // Variadisches Klassentemplate, um Duplikation der Information über Basisklassen zu vermeiden
    template <typename... T>
    struct Inherit
    {
    	virtual unsigned int ClassID() const
    	{
    		return 0;
    	}
    };
    
    template <typename Head, typename... T>
    struct Inherit<Head, T...> : Head, Inherit<T...>
    {
    	virtual unsigned int ClassID() const
    	{
    		return Head::ClassID() | Inherit<T...>::ClassID();
    	}
    };
    
    // Test, ob Objekt d von b erbt
    template <typename D, typename B>
    bool Implements(const D& d, const B& b)
    {
    	return (d.ClassID() & b.ClassID()) != 0;
    }
    

    Die Anwendung ist extrem einfach:

    #include <iostream>
    
    struct BaseX : Base<BaseX> {};
    struct BaseY : Base<BaseY> {};
    struct BaseZ : Base<BaseZ> {};
    
    struct DerivedA : Inherit<BaseX> {};
    struct DerivedB : Inherit<BaseX, BaseY> {};
    struct DerivedC : Inherit<BaseX, BaseY, BaseZ> {};
    
    template <typename D, typename B>
    void Test(const D& d, const B& b, const char* dname, const char* bname)
    {
    	const char* relation = Implements(d, b) ? "" : " nicht";
    	std::cout << "Objekt " << dname << " implementiert " << bname << relation << ".\n";
    }
    
    #define TEST(d, b) Test(d, b, #d, #b)
    
    int main()
    {
    	BaseX x;
    	BaseY y;
    	BaseZ z;
    	DerivedA da;
    	DerivedB db;
    	DerivedC dc;
    
    	// Statische Typinformationen wegwerfen
    	BaseX& a = da;
    	BaseX& b = db;
    	BaseX& c = dc;
    
    	TEST(a, x);
    	TEST(a, y);
    	TEST(a, z);
    	TEST(b, x);
    	TEST(b, y);
    	TEST(b, z);
    	TEST(c, x);
    	TEST(c, y);
    	TEST(c, z);
    }
    


  • Hrmmmm.... hrmmm! Das könnte ich in meinen derzeitigen Ansatz als Optimierung für die "Implements"-Routine natürlich einpflegen, danke für den Tipp! Im Moment sind die Ids nur stinknormale fortlaufende Zahlen, die eben als Statische Member beim Programmstart verteilt werden, oder halt zur Laufzeit, wenn etwas dynamisches in's Spiel kommt.

    Als lustige Beinote: Ich hab' ja eben schon auf die Konstruktor-Problematik hingewiesen, also habe ich die Ableitung aus dem "Reflects" herausgenommen, allerdings hatte ich dann auf einmal das Problem, dass das "vtable"-Zeiger-Analog in den Basisklassen offenbar nicht gesetzt wurde. Aber das lag nur daran, dass ich in der falschen Reihenfolge abgeleitet habe... Also mein Ansatz ist auch problematisch, wenn man nicht darauf achtet...


  • Mod

    Nur ins Blaue hinein geraten: könnte es sein, dass der ICE nicht auftritt, wenn dem Compiler /vmv oder /vmm mitgegeben wird?
    Wenn ja, dann wäre das Nichtfunktionieren by Design; die Tatsache, dass es zum ICE kommt und keine ordentliche Fehlermeldung dann nat. trotzdem ein Bug.



  • Hallo!
    Danke, für den Tipp, ich habe gerade mal nacheinander alle drei Optionen explizit für den minimalen Testcase angegeben, sie hatten keinen Einfluss auf den ICE.



  • Zumal laut Doku auch die allgemeinste Repräsentation "/vmv" die Normal-Einstellung sei und ich daran nie etwas gedreht habe, es sei denn das passierte irgendwie implizit.



  • Also weitere Detail zum Fehler: Der tritt nur auf, wenn der Methoden-Zeiger sozusagen "die Klasse verlässt", also wie in dem Beispiel bei einem "unabhängigen" Typ "template_func" mitgegeben wird um dann hintenrum wieder in der Klassenhierarchie zu landen. Deshalb war mein erster Repro-Versuch auch fehlerhaft, da ich eine Basisklasse direkt den Zeiger mitgegeben hatte.



  • Hrmmm, nachdem das alles erstmal funktioniert hat und ich nun bei der Vereinfachung bin (das war zwischendurch ziemlich undurchsichtig^^), bin ich etwas verblüfft, wie mächtig doch die Funktionsüberladungs-Auflösung vor allem im Zusammenspiel mit decltype ist. Mir kommt es im Moment so vor, als wäre sie in VC++ vielleicht etwas zu mächtig?

    Folgendes Beispiel:

    template< typename... Ts >
    struct list {};
    
    template< typename Class, typename... Args >
    struct some_base {};
    
    struct some_struct : 
        public some_base< some_struct, int, float, double >
    {};
    
    template< typename Class, typename... Args >
    list<Args...> deduce_args( some_base<Class,Args...>& );
    
    int main() {
       some_struct val;
       decltype(deduce_args(val)) thelist;
    }
    

    Und das kompiliert bei mir durch! Nicht nur muss er also val zur Basisklasse casten, ohne dass er eigentlich den exakten Typ des Arguments kennt, danach erst kann er damit anfangen gegen die Template-Argumente zu matchen. Und die Template-Basisklassen sind ja eigentlich gar nicht zusammengehörig. Klar ist das alles möglich, aber an anderen Stellen stellt sich der Compiler doch auch so blöde an, warum geht er hier so weit? Ist das überhaupt legal? Und wenn ja, welche Regeln befolgt er hier?



  • Und weiter geht's im Karton mit dem Ausnutzen der Overload-Resolution:

    #include <iostream>
    #include <type_traits>
    using namespace std;
    
    template< typename Class, typename BaseList, typename InterfaceList >
    struct Reflects {};
    
    template< typename Class, typename BaseList, typename InterfaceList >
    std::true_type is_reflecting_impl(Class*, Reflects<Class, BaseList, InterfaceList >* );
    
    template< typename Class >
    std::false_type is_reflecting_impl(Class*, ...);
    
    template< typename T >
    struct is_reflecting : public decltype(is_reflecting_impl<T>((T*)nullptr, (T*)nullptr))
    {};
    
    struct A : public Reflects< A, int, float >
    {};
    
    struct B {};
    
    struct C : public A, public Reflects< C, float, double > {};
    
    struct D : public A {};
    
    int main() {
    	std::cout << is_reflecting<A>::value << "\n";
    	std::cout << is_reflecting<B>::value << "\n";
    	std::cout << is_reflecting<C>::value << "\n";
    	std::cout << is_reflecting<D>::value << "\n";
    
    	return 0;
    }
    

    Das spart eine Pseudo-Basisklasse "ReflectingClass<T>", die ich ohne Ausnutzen der Overload-Resolution gebraucht hätte, um per Template-Spezialisierung herauszufinden, ob eine Klasse reflektiert.
    Irgendwie ist das viel mächtiger als direkte Template-Spezialisierung Oo

    Aber das einzige, was ich dazu finde sind ein paar Dokumente zur Overload-Resolution von IBM, die geben aber soweit ich das sehe kein Beispiel dazu, wie der Compiler zuerst zu einer Basisklasse castet und dann erst versucht das Template zu matchen...
    Kann mir jemand den Paragraphen im Standard nennen, der erklärt wie das funktioniert? Vielleicht finde ich noch irgendwo einen Draft.


  • Mod

    14.8.2.1 /4



  • Alles klar, vielen Dank, Du bist echt ein Lexikon! 😉 Es ist also explizit eine Regel im Standard integriert worden, damit das funktioniert. Kam die Regel erst im Zuge von decltype hinzu, oder funktionierte das schon immer?
    Ich hatte jetzt befürchtet, dass der Grund aus dem das funktioniert inhärent im restlichen Überladungs-Mechanismus (Also Kombination von einzelnen Eigenschaften) steckte, sodass ich davon ausging, dass ich da etwas fundamental nicht verstanden habe...
    Wie dem auch sei, schön dass es die Regel gibt 🙂 Immerhin habe ich jetzt alles zusammengenommen den ICE umschiffen können und wieder eine Menge gelernt.


  • Mod

    decimad schrieb:

    funktionierte das schon immer?

    ja


Anmelden zum Antworten