Reflektion reflektieren...



  • decimad schrieb:

    Habe ich irgendwas übersehen, was ich auf jeden Fall noch bedenken sollte?

    Keine Ahnung, das ist immer schwer zu sagen. D.h., im Endeffekt hast du überhaupt kein richtiges Reflection, sondern willst es erstmal in C++ nachbilden? Also auch kein Qt?
    Was bei dir das Binding Iface und der Adapter machen ist mir nicht ganz klar. Sowas ähnliches haben wir im System auch, also ein "Mapping" für normale C++ Klassen, die ohne Qt oder Codegenerierung oder sonstiges so eine Art Reflection anbieten sollen. Das ist aber nicht invasiv und unabhängig von den Klassen. Es wird ein externes Mapping aufgebaut, wo man Strings auf "irgedwas" mappt, um an die Daten zu kommen, z.B. auf boost::function (oder std), die auf eine Memberfunktion zeigt. Dann gibt es lauter vorgefertige Blöcke, um z.B. auf Listen zuzugreifen. Und dann gibts Klassen, die mit diesen Mappings was anfangen können, z.B. um die Werte an die GUI zu binden.



  • Ganz genau, ich habe nichts zur Verfügung außer einem Satz von Hilfsklassen, die Interface-Reflektion ala Query/Enum-Interface über Klassenhierarchien ermöglichen. So etwas wollte ich dann in die Bindings implementieren, um an die Schnittstellen zu kommen, bzw. sie aufzulisten. Das heißt, den Teil habe ich soweit zur Verfügung. Im Prinzip könnte ich die Bindings auch on-the-fly generieren, aber das Problem ist dann halt, dass ich dann zunächst nichts greifbares habe, um Observer auf den Schnittstellen zu halten, also die Bindung "Objekt/Property/Fragment->Binding" wäre nicht vorhanden. Darum wollte ich jetzt diese Binding-Objekte, welche dann die Schnittstellen implementieren und als Adapter zum eigentlichen Objekt/Member/Methoden fungieren, als echte Datenmember pro Objekt ablegen. Eine Möglichkeit das zu umgehen wäre Observer-Listen statisch in den Binding-Objekten abzulegen (mit Zeiger auf das adaptierte Objekt). Andererseits müsste man die Listen pro Objekt sortieren, also auch nicht gänzlich unaufwändig... Speicher vs. Laufzeit halt.

    Im Moment würde das irgendwie so aussehen im Client-Code:

    class my_class : public reflected_t<my_class> {
    public:
        my_class()
            : something_binder(this), root_group(this)
        {}
    
        void set_something(int foo) {
             // ...
    
             // Observer benachrichtigen.
             something_binder.notify( &simple_observer::value_changed );
        }
    
        int get_something() const;
    
        binding get_data_binding() override {
            return root_group;
        }
    
        const_binding get_data_binding() const override {
            return root_group;
        }
    
    private:
        INTEGRAL_METHOD_BINDER(get_something, set_something) something_binder;
    
        static const char something_id[];
    
        // Käme natürlich auch in ein Makro...
        static_id_group< my_class,
             id_element< something_id, decltype(&my_class::something_binder), &my_class::something_binder >
        > root_group;
    };
    

    Das Binding-Iface ist so etwas wie "integer":

    class integer : public binder_base, public observable<simple_observer>
    {
        virtual void set( int value ) = 0;
        virtual int get() const = 0;
        virtual std::pair<int, int> limits() const = 0;
    
        // der Einfachheit halber hier type_info
        const type_info* type() const override {
            return typeid(integer);
        }
    
        const char* name() const override {
            return "integer";
        }
    };
    

    Ist jetzt aus den Fingern gesogen. Der Adapter mapped diese Schnittstelle auf "set_something" und "get_something" statisch, wie im Beispiel da oben. Aber wenn der Datentyp dort meinetwegen short wäre, würde er auch die casts machen. Ebenso könnte es auch eine Implementierung über std::function<> geben, wenn man irgendwie Parameter mitgeben muss. Um diese Belange kümmern sich die Adapter. Der "Binder" bringt die Binding-Schnittstelle "integer" mit dem Adapter zusammen. Im Prinzip sind das nur Trait-basierte Templates gerade, nach außen treten dann nur die Schnittstellen im Binding zutage.

    Von außen könnte man jetzt sagen:

    auto binding = my_obj.get_data_binding();
    
       if( binding.supports<id_group>() ) {
           auto* ptr = binding.as<id_group>();
    
           for( unsigned int i=0; i<ptr->num_elements(); ++i) {
                cout << ptr->id(i) << "\n";
    
                auto elem_binding = ptr->element(i);
                cout << elem_binding.name() << "\n";
    
                // entsprechend "edit_widget::register_controller( integer::static_type(), edit_integer_controller_factory )"
                auto edit_controller = edit_widget::lookup_controller( elem_binding );
           }
       }
    

    Natürlich würde ich wohl nur selten direkt den Typ im Code abfragen, sondern eben Listen mit Factories für bestimmte Typen vorsehen.



  • Auch mit Code find ich das noch etwas verwirrend ^^

    D.h., der Binder kümmert sich jeweils um einen Wert? Wo wird diese "integer" Klasse verwendet?

    Was mir nicht gefällt, ist dass die Klassen irgendwas über die Binding wissen müssen. Ich hätte das komplett extern gemacht. Und so wie ich das sehe, könntest du das auch machen. Es gibt doch eigentlich keinen Grund, warum my_class von reflected_t ableiten und irgendwas über bindings wissen müsste.



  • Hey Mechanics,

    ja wie ich ja sagte, im Prinzip könnte ich das schon extern machen, mit einem großen Problem: Wie tracke ich Veränderungen am Objekt, wenn dann nicht der komplette Zugriff nur noch über die Reflektion stattfindet? Wie würde die UI mitbekommen, dass sich ein Wert verändert hat, beispielsweise. Also irgendwie müssen die Objekte schon zu einem gewissen Grad wissen, dass dort eventuell jemand lauscht.

    Diese Tatsache mal außen vor bin ich aber gerade dabei, das mal probeweise "extern" aufzuziehen. Also mit Meta-Klassen, von denen man mit der Referenz auf ein "Objekt" Objektrefenzen erzeugen kann, die man dann durchbrausen kann, was noch mehr Subobjektreferenzen erzeugt usw. usf.. Also alles on-the-fly aus einer Beschreibung wie (im Moment):

    struct sample_struct : public up::reflecting_object_t<sample_struct> {
    	int get() const {
    		return 5;
    	}
    };
    
    up::class_t< sample_struct,
    	up::integer_getter_binder<sample_struct, &sample_struct::get >
    > sample_class;
    
    int test() {
    	sample_struct sample;
    
    	up::reflecting_object& obj = sample;
    
    	auto ref = sample_class.bind(obj);
    	std::cout << ref.obj_->num_members();
    	auto member_ref = ref.obj_->member_ref(0);
    	auto* the_class = member_ref.obj_->get_class();
    
    	return 4;
    
    }
    

    Ganz schöner Wust von Objekten, Klassen usw., da verliert man schnell den Überlick. Muss ich mich schon noch ein bissl reinfuchsen, bis ich das alles klar vor Augen habe.

    Was ich mich nur eben frage ist, wie ich das am besten mit Changed-Events usw. durchführe.



  • Was wir in der Arbeit geschrieben haben, schaut irgendwie so aus:

    Binding b;
    b.bindMember("value", &MyClass::value);
    b.bindMethod("count", &myClass::getCount);
    

    Weiß jetzt nicht, wie das alles genau heißt/aufgebaut ist, muss jedesmal nachschauen. Jedenfalls bekommen die "Bindingmember" String Namen. Damit kann man die Bindings z.B. benutzen, um XML/Json zu serialisieren. So unübersichtlich ist die Basisversion eigentlich nicht. Wird aber dadurch unübersichtlich, dass wir auch recht komplexe Konstrukte unterstützen wollten.
    z.B. sowas:

    struct MyStruct
    {
      std::map<std::string, ISomeBaseIface*> plugins;
    };
    

    Also z.B. eine Map, die Zeiger auf polymorphe Objekte hält, die jeweils unterschiedliche eigene Bindings haben können. Und sowas wollen wir auch serialisieren können. Dann gibts noch verschiedene Möglichkeiten, irgendwelches Verhalten anzupassen, z.B. in welchem Format jetzt ein Datum gespeichert wird, oder dass irgendwelche Member als Attribute im XML landen. Und da wir viel mit Qt arbeiten, gibts noch zusätzlich Unterstützung für QVariants und QObjects, was noch mehr Dynamik reinbringt. Ist dadurch insgesamt natürlich schon recht komplex geworden.
    Wir haben keinen Tracking Mechanismus gebraucht. Wüsste jetzt spontan nicht, wie man das am besten machen könnte, dürfte aber machbar sein. Ich hätte das wahrscheinlich am ehesten optional gemacht. Das wäre schon etwas, was eine Klasse unterstützen kann, dann muss sie halt 1-2 Methoden wie addObserver/removeObserver anbieten, dann kann das Binding über SFINAE feststellen, ob die Klasse observable ist. Die Klasse würde nur eine Methode mit einem string aufrufen, z.B. valueChanged("count"). Ist aber nur eine spontane Idee und natürlich Geschmackssache, wie man das macht, dein Stil könnte natürlich wieder ganz anders ausschauen.



  • Hey,

    Jau, die Klassen-Meta-Objekte sowie die Binder könnte ich in diesem "design" auch zur Laufzeit zusammenbasteln und dann eben Container statt konstante Arrays beim Lookup benutzen und ähnliches, die erste Implementierung der Schnittstellen mache ich hier aber erstmal mit Templates soweit es geht. Eigentlich bin ich der Meinung, dass man das aber auch gut mixen kann, in der Hierarchie. Wie Du schon sagst, wenn man im Objekt Listen von reflektierten Objekten hat, wäre auch ein hervorragendes Binding möglich ala "object_list"- oder "object_map"-Member usw. Ich will halt immer ausprobieren, was man so compile-zeit-konstant hinbekommt. Member-Ids kommen natürlich auch noch, aber das war jetzt die geringste Sorge 😉 Ich werd' vielleicht ab und zu mal Fortschritte posten, immer schön, wenn man von außen dann hört, was man besser machen könnte etc.

    Danke Dir soweit auf jeden Fall, ich denke ich bin auf einem gangbaren Weg und bin vor allem nicht mehr so sehr am Zweifeln ob das alles Sinn ergibt, hab hier ja auch ein recht einzigartiges Szenario.



  • Okay, also man öffnet damit glaube ich so ein bisschen die Büchse der Pandora oder wie soll ich's ausdrücken 😉

    Ich hab' jetzt mal alles komplett aus den reflektierten Klassen rausgenommen, das Reflektions-System läuft parallel dazu. Hier ein Beispiel: http://pastebin.com/iA7TahP3

    Für jede reflektierte Klasse gibt's ein Objekt abgeleitet vom Typ "class_". Es gibt mappings von type_info -> const class_, string -> const class_.
    Objekt-Referenzen sind praktisch Paare aus "const class_" und dazu passendem "void".

    Jetzt gibt's ein Problem. Erst einmal bin ich nicht daran interessiert integrale Typen direkt nach außen zu reflektieren, ich muss ja irgendwie die Menge der Typen reduzieren, ansonsten ist das System unbrauchbar. In dem Beispiel ist schon einmal die Menge der integralen Integer-Typen auf ein Interface "integer" runtergebrochen. Wie man in dem Beispiel auch sieht, braucht man dafür hinter den Vorhängen Proxy-Objekte (die werden von den adapter-Klassen erzeugt), die diese Schnittstellen implementieren und diese auf die konkreten Objekte umbiegen. Und das sorgt für ein Problem: Wenn man typeid auf die entstehenden Objekte ausführt, bekommt man natürlich das type_info für diese Proxy-Typen.
    Wenn man nun eine Referenz auf eine Basis-Schnittstelle hat (und sowas hat man ja oft) oder nur ein solches hat um eine Reflektions-Referenz zu erzeugen und möchte den konkreten Typ, braucht man aber aus offensichtlichen Gründen erst einmal eine Referenz auf den Meist-abgeleiteten Typ (nicht exakt, die Proxy-Typen kann man sich eigentlich sparen). Aber da man da wegen der Externität nicht vom System her drankommt, bleibt nur typeid. Das aber sorgt dafür, dass man für jeden Proxy-Typ auch eine class_ registrieren muss, weil typeid ja die Typ-Id der Proxy-Typen liefert. Wegen des Runterbrechens der konkreten Typen auf Schnittstellen gibt es aber für die Mehrzahl der reflektierten Member Proxy-Klassen, sodass ich auf einmal zur Laufzeit auch mit sehr vielen Proxy-Meta-Klassen-Objekten zu tun hätte, die für sonst nichts zu gebrauchen sind.
    Eine andere Möglichkeit wäre, dass man ab der Konstruktion der Objekte in irgendwelchen Factories oder aus der Reflektions-Deserialisierung immer eine Referenz auf die meistabgeleitete Subklasse eines Objekts mitführt (Minus Proxy). Dann ist das System zwar immernoch transparent für die reflektierten Klassen, aber dafür zieht sich dann Reflektionscode durch alle anderen Teile des Programms. Also als Beispiel:

    // Überall wo Objekte verwaltet werden:
    struct holder {
        // Normaler C++-Code über Schnittstellen
        std::unique_ptr< some_base_iface > some_ptr;
    
        // Reflektionskram
        // Referenz zum "Meist-abgeleiteten Typ" minus Proxy-Typ.
        up::mutable_object_ref some_ref;
    };
    
    std::vector< holder > my_objects;
    

    Das heißt man müsste überall die Referenz für die Meistabgeleitete Nicht-Proxy-Klasse mitschleifen.

    Ich glaube mir fehlen so ein bisschen die Worte um das kompakt auszudrücken. Ist verständlich was ich meine, bzw. übersehe ich da eine dritte Alternative, die ohne einen Wust aus Proxy-Metaklassen oder Eingriffe überall in den Code auskommt?

    Viele Grüße



  • Hrmmm, wenn ich so drüber nachdenke, dann ist das eigentlich kein realistisches Problem. Die Proxies treten ja nur auf, wenn ich Member aufliste, dort weiß ich aber die echten Typen und gebe Meta-Referenzen raus, kann also die richtigen Referenzen rausgeben, also die meistabgeleitete Klasse, die ich benötige. Anderswo entstehen die Teile aus der Deserialisierung oder in Factories... Die entstehenden Objekte haben dann sinnigerweise sowieso eine Meta-Klasse, also kann ich mit typeid hochcasten.
    Damit gibt's jetzt nur noch das Problem, dass ich das hochcasten zum Metaklassentyp von typeid eigentlich nur über eine Pfadsuche erledigen kann 🙂 Klingt ein bisschen frickelig...



  • decimad schrieb:

    Hrmmm, wenn ich so drüber nachdenke, dann ist das eigentlich kein realistisches Problem. Die Proxies treten ja nur auf, wenn ich Member aufliste, dort weiß ich aber die echten Typen und gebe Meta-Referenzen raus, kann also die richtigen Referenzen rausgeben, also die meistabgeleitete Klasse, die ich benötige.

    Ist nicht so einfach nachzuvollziehen, was du schreibst 😉
    Bin mir nicht sicher, ob du mit deiner letzten Aussage sowas berücksichtigst:

    std::vector<std::unique_ptr<some_base_iface>> objects;



  • Hey Mechanics,

    für std::vector< std::unique_ptr< > > (Ohne den IFace-Teil sozusagen), gibt's dann nen Proxy ala "list" oder wenn ich fleißig bin "random_access" oder irgendwie so, und some_base_iface ist dann ja wieder der Fall, in dem man eine Meta-Klasse für das konkrete Objekt zur Hand hat (lookup über typeid) oder halt nur Zugriff auf die Meta-Klasse des Interfaces bekommt. So stelle ich mir das gerade vor. Wenn ein Objekt serialisierbar sein soll, muss ich also eine Meta-Klasse dafür haben, oder könnte auch schauen, ob das Interface ein Serialisierungspart beinhaltet, das kann man ja zur Laufzeit überprüfen (Ableitungsgraph der Schnittstelle wäre zur Laufzeit ja vorhanden).



  • Gnrml. Ich hab' jetzt nach einer Unterbrechung wieder Zeit mich damit zu beschäftigen und immer wenn ich ne längere Pause einlege, sehe ich die Dinge hinterher anders.
    Angefangen hatte es ja mit Metainformationen für Klassen, aber irgendwie interessiert's doch eigentlich nicht, ob es ein Klassentyp ist, oder ein Funktionstyp oder was auch immer. Was zählt ist, was man mit den Objekten anstellen kann.
    Daher wäre mein Gedanke jetzt, das einfach nur noch "Typ" zu nennen, der Member haben kann und sozusagen in Schnittstellen bzw. Subtypen zerfallen kann. Also ein Objekt eines Typs zerfällt dann beispielsweise in eine Liste der Schnittstellen oder "Fragmente", die sie anbietet. Für Klassenobjekte wären das die sinnigerweise eine Auswahl der Basisklassen.

    Nach meinem derzeitigen Ansatz sieht eine Objektreferenz irgendwie so aus:

    template< typename Type >
    class object_ref {
       type* type_;   // Laufzeit Metatyp
       Type* object_;
       intrusive_ptr<refcounted> lifetime_; // Falls Proxies im Spiel sind oder die Lebenszeit an diese Referenz gebunden ist.
    };
    

    Objekte ohne Compile-Zeit-Typen werde dann einfach in object_ref<(const) void> gehalten.
    Im Prinzip könnte ich die Lebenszeit von Proxies auch über den type* Metatyp regeln, dann bräuchte man den dritten Zeiger in der Referenz nicht, aber das hätte zur Folge:
    - Für jeden Proxy-Typ bräuchte man einen Proxy-Metatyp
    - Wenn die Referenz die Lebenszeit bestimmt, bräuchte man einen Pointer-to-Metatyp und ähnliches
    - Der Metatyp-Zeiger wäre kein Vergleichskriterium mehr

    Ferner stellt sich noch die Frage ob man denn Referenzen auf ein Objekt wie Referenzen auf eine Schnittstelle bzw. ein "Typfragment" des Objekts behandeln sollte, oder ob das zwei getrennte Dinge sind. Also nach dem Motto:

    class fragment_ref {
        void* fragment_ptr;
        type* fragment_type;
    };
    
    void func( object_ref<void> obj )
    {
        // A) Für die Lebenszeit muss man die Objektreferenz behalten
        Iface* iface_ptr = obj.query<Iface>();
    
        auto ref = obj.fragment(); // Der Typ des Objekts selber sozusagen
    
        for( auto fragment : ref.fragments() ) {
           // Subtypen oder Schnittstellen, die vom Objekt unterstützt werden
        }
    
        // B) Lebenszeit auch an Schnittstellenreferenzen gebunden,
        //    wobei die sich wie Objektreferenzen verhalten
        //    Das funktioniert überhaupt nur, wegen des intrusive_ptr's, der
        //    sich transparent um die Zerstörung des konkreten Objekts/Proxies
        //    kümmert (ansonsten müsste man zur Zerstörung immer zur Laufzeit
        //    zum konkreten Objekttyp hochcasten -> "langsam", ginge aber auch).
        object_ref<Iface> iface_ref = cast<Iface>(obj);
    }
    

    Also sollte man Objekte und "Fähigkeiten" dieser Objekte getrennt behandeln, oder eher nicht? Irgendwie ist das ja ein Tradeoff zwischen dem was "richtig (tm)" ist, und dem, was für den Benutzer des Systems praktisch ist.
    Ein Vorteil des Trennung wäre, dass die Objektrefenz sozusagen eine Identität für das Objekt darstellt, insbesondere sind keine Upcasts notwendig, um von einer Typ-Referenz erstman auf die Objektreferenz zu kommen und von dort wieder runterzucasten, um an eine andere Typreferenz zu kommen.

    Außerdem: Im Moment haben Objekte Subtypen (Fragmente) und Member, ist eine Trennung zwischen Datenmember und Funktionsmember überhaupt sinnvoll? Letztlich sind das ja alles Objekte, die einen haben halt eine Datentyp-Schnittstelle, die anderen sind eben "Callable" usw.

    Also das technische Konstrukt funktioniert soweit (und ist bei allen Ansätzen ja ziemlich gleich), aber ich bin von keinem der Ansätze bisher so sehr überzeugt, dass ich ihn mit all seinen Folgen guten gewissense umsetzen kann, schließlich hat das ja Auswirkung darauf, wie all der Client-Code gestrickt ist.

    Habt ihr Meinungen oder Vorschläge, was ich mir da mal anschauen könnte?

    Viele Grüße


Anmelden zum Antworten