325 Stimmen

Wie kann ich Reflection zu einer C++-Anwendung hinzufügen?

Ich möchte in der Lage sein, eine C++-Klasse auf ihren Namen, ihren Inhalt (d.h. Mitglieder und ihre Typen) usw. zu untersuchen. Ich spreche hier von nativem C++, nicht von verwaltetem C++, das Reflection hat. Ich weiß, dass C++ mit RTTI einige begrenzte Informationen liefert. Welche zusätzlichen Bibliotheken (oder andere Techniken) könnten diese Informationen liefern?

21 Stimmen

Pech gehabt, ohne Makros und andere Vorverarbeitungen geht es nicht, denn die erforderlichen Metadaten gibt es nicht es sei denn, Sie erstellen sie manuell mit Hilfe von Makro-Vorverarbeitungsmagie.

7 Stimmen

Die Informationen, die Sie von RTTI erhalten können, reichen jedoch nicht aus, um die meisten Dinge zu tun, für die Sie eigentlich eine Reflexion benötigen würden. Sie können zum Beispiel nicht über die Mitgliedsfunktionen einer Klasse iterieren.

11voto

Michel Punkte 1426

Ich habe so etwas schon einmal gemacht, und obwohl es möglich ist, ein gewisses Maß an Reflexion und Zugang zu höherwertigen Funktionen zu erhalten, lohnt sich der Wartungsaufwand möglicherweise nicht. Mein System wurde verwendet, um die UI-Klassen vollständig von der Geschäftslogik durch Delegation ähnlich dem Objective-C-Konzept der Nachrichtenübermittlung und Weiterleitung getrennt zu halten. Dazu muss man eine Basisklasse erstellen, die in der Lage ist, Symbole (ich habe einen String-Pool verwendet, aber man könnte es auch mit Enums machen, wenn man Geschwindigkeit und Kompilierzeit-Fehlerbehandlung der totalen Flexibilität vorzieht) auf Funktionszeiger abzubilden (eigentlich keine reinen Funktionszeiger, sondern etwas Ähnliches wie Boost mit Boost.Function - worauf ich zu der Zeit keinen Zugriff hatte). Sie können dasselbe für Ihre Mitgliedsvariablen tun, solange Sie eine gemeinsame Basisklasse haben, die jeden Wert darstellen kann. Das gesamte System war eine unverfrorene Abzocke von Key-Value Coding und Delegation, mit einigen Nebeneffekten, die vielleicht die schiere Menge an Zeit wert waren, die nötig war, um jede Klasse, die das System verwendete, dazu zu bringen, alle ihre Methoden und Mitglieder mit legalen Aufrufen abzustimmen: 1) Jede Klasse konnte jede Methode jeder anderen Klasse aufrufen, ohne Header einfügen oder falsche Basisklassen schreiben zu müssen, so dass die Schnittstelle für den Compiler vordefiniert werden konnte; und 2) Die Getter und Setter der Mitgliedsvariablen waren leicht thread-sicher zu machen, da das Ändern oder der Zugriff auf ihre Werte immer über zwei Methoden in der Basisklasse aller Objekte erfolgte.

Es führte auch zu der Möglichkeit, einige wirklich seltsame Dinge zu tun, die sonst nicht einfach in C++ sind. Zum Beispiel konnte ich ein Array-Objekt erstellen, das beliebige Elemente eines beliebigen Typs enthielt, einschließlich sich selbst, und neue Arrays dynamisch erstellen, indem ich eine Nachricht an alle Array-Elemente übergab und die Rückgabewerte sammelte (ähnlich wie map in Lisp). Ein weiterer Punkt war die Implementierung der Key-Value-Beobachtung, mit der ich die Benutzeroberfläche so einrichten konnte, dass sie sofort auf Änderungen in den Elementen der Backend-Klassen reagiert, anstatt die Daten ständig abzufragen oder die Anzeige unnötig neu zu zeichnen.

Interessanter für Sie ist vielleicht die Tatsache, dass Sie auch alle für eine Klasse definierten Methoden und Mitglieder ausgeben können, und zwar in Form von Strings.

Nachteile des Systems, die Sie davon abhalten könnten, sich die Mühe zu machen: Das Hinzufügen aller Nachrichten und Schlüsselwerte ist extrem mühsam; es ist langsamer als ohne jegliche Reflexion; Sie werden es hassen, die boost::static_pointer_cast y boost::dynamic_pointer_cast Die Einschränkungen des Systems mit starker Typisierung sind immer noch da, man versteckt sie nur ein wenig, damit es nicht so offensichtlich ist. Tippfehler in Ihren Strings sind auch keine lustige oder leicht zu entdeckende Überraschung.

Wie man so etwas implementiert: Verwenden Sie einfach gemeinsame und schwache Zeiger auf eine gemeinsame Basis (meine hieß sehr einfallsreich "Object") und leiten Sie für alle Typen, die Sie verwenden möchten, ab. Ich würde empfehlen, Boost.Function zu installieren, anstatt es so zu machen, wie ich es gemacht habe, nämlich mit etwas benutzerdefiniertem Mist und einer Menge hässlicher Makros, um die Funktionszeigeraufrufe zu verpacken. Da alles gemappt ist, ist die Untersuchung von Objekten nur eine Frage der Iteration durch alle Schlüssel. Da meine Klassen waren im Wesentlichen so nah an eine direkte ripoff von Cocoa wie möglich mit nur C++, wenn Sie so etwas wollen, dann würde ich vorschlagen, mit der Cocoa-Dokumentation als eine Blaupause.

0 Stimmen

Hey, @Michael; hast du noch den Quellcode dafür, oder hast du ihn entsorgt? Ich würde ihn mir gerne mal ansehen, wenn es dir nichts ausmacht.

0 Stimmen

Ups, Ihr Name ist falsch geschrieben! Nein Wunder Ich habe nie eine Antwort erhalten

9voto

user4385 Punkte 89

Die beiden reflexionsähnlichen Lösungen, die ich aus meiner C++-Zeit kenne, sind:

1) Verwenden Sie RTTI, das Ihnen ein Bootstrap zum Aufbau Ihres reflexionsähnlichen Verhaltens bietet, wenn Sie alle Ihre Klassen von einer "Objekt"-Basisklasse ableiten können. Diese Klasse könnte einige Methoden wie GetMethod, GetBaseClass usw. bereitstellen. Wie diese Methoden funktionieren, müssen Sie manuell einige Makros hinzufügen, um Ihre Typen zu dekorieren, die hinter den Kulissen Metadaten im Typ erstellen, um Antworten auf GetMethods usw. zu liefern.

2) Eine andere Möglichkeit, wenn Sie Zugang zu den Compiler-Objekten haben, ist die Verwendung der DIA SDK . Wenn ich mich richtig erinnere, können Sie damit pdbs öffnen, die Metadaten für Ihre C++-Typen enthalten sollten. Das könnte ausreichen, um das zu tun, was Sie brauchen. Diese Seite zeigt, wie man zum Beispiel alle Basistypen einer Klasse ermitteln kann.

Diese beiden Lösungen sind allerdings etwas hässlich! Es geht nichts über ein bisschen C++, um die Vorzüge von C# zu schätzen.

Viel Glück!

0 Stimmen

Das ist raffiniert und ein riesiger Hack, mit dem DIA SDK, was Sie dort vorgeschlagen haben.

6voto

Luis Punkte 859

Ich denke, der Artikel "Using Templates for Reflection in C++" von Dominic Filion könnte für Sie interessant sein. Er befindet sich in Abschnitt 1.4 von Spieleprogrammierung Gems 5 . Leider habe ich mein Exemplar nicht dabei, aber suchen Sie danach, denn ich denke, es erklärt, was Sie fragen.

6voto

Matthieu M. Punkte 266317

Diese Frage ist schon etwas älter (ich weiß nicht, warum ich heute immer wieder auf alte Fragen stoße), aber ich habe mir Gedanken gemacht über BOOST_FUSION_ADAPT_STRUCT die die Reflexion zur Kompilierzeit einführt.

Es liegt natürlich an Ihnen, dies auf die Laufzeitreflexion abzubilden, und es wird nicht allzu einfach sein, aber es ist in dieser Richtung möglich, während es in der umgekehrten Richtung nicht möglich wäre :)

Ich denke wirklich, dass ein Makro zur Kapselung der BOOST_FUSION_ADAPT_STRUCT könnte man die notwendigen Methoden generieren, um das Laufzeitverhalten zu erhalten.

2 Stimmen

Von minghua (der den Beitrag ursprünglich bearbeitet hat): Ich habe mich mit dieser BOOST_FUSION_ADAPT_STRUCT-Lösung beschäftigt und bin schließlich auf ein Beispiel gestoßen. Siehe diese neuere SO Frage - C++ iteriert in verschachtelte Strukturfelder mit boost fusion adapt_struct .

0 Stimmen

Großartig, Matthieu! Ich habe gerade bemerkt, dass ich im Laufe des letzten Jahres hier und da Ihre Hinweise gesehen habe. Bis jetzt habe ich nicht bemerkt, dass sie zusammenhängen. Sie waren sehr inspirierend.

5voto

TheNitesWhoSay Punkte 127

El RareCpp Bibliothek sorgt für eine relativ einfache und intuitive Reflexion - alle Feld-/Typinformationen sind so konzipiert, dass sie entweder in Arrays verfügbar sind oder sich wie ein Array-Zugriff anfühlen. Es ist für C++17 geschrieben und arbeitet mit Visual Studios, g++ und Clang. Die Bibliothek ist eine reine Header-Bibliothek, d.h. Sie müssen nur "Reflect.h" in Ihr Projekt kopieren, um sie zu verwenden.

Reflektierte Strukturen oder Klassen benötigen das REFLECT-Makro, bei dem Sie den Namen der Klasse, die Sie reflektieren, und die Namen der Felder angeben.

class FuelTank {
    public:
        float capacity;
        float currentLevel;
        float tickMarks[2];

    REFLECT(FuelTank, capacity, currentLevel, tickMarks)
};

Das ist alles, es ist kein zusätzlicher Code zur Einrichtung der Reflexion erforderlich. Optional können Sie Klassen- und Feldannotationen bereitstellen, um Superklassen zu durchlaufen oder zusätzliche Kompilierzeitinformationen zu einem Feld hinzuzufügen (z. B. Json::Ignore).

Das Durchlaufen von Feldern kann so einfach sein wie...

for ( size_t i=0; i<FuelTank::Class::TotalFields; i++ )
    std::cout << FuelTank::Class::Fields[i].name << std::endl;

Sie können eine Objektinstanz in einer Schleife durchlaufen, um auf Feldwerte (die Sie lesen oder ändern können) und Feldtypinformationen zuzugreifen...

FuelTank::Class::ForEachField(fuelTank, [&](auto & field, auto & value) {
    using Type = typename std::remove_reference<decltype(value)>::type;
    std::cout << TypeToStr<Type>() << " " << field.name << ": " << value << std::endl;
});

A JSON-Bibliothek baut auf RandomAccessReflection auf, die automatisch geeignete JSON-Ausgabedarstellungen zum Lesen oder Schreiben identifiziert und rekursiv alle reflektierten Felder sowie Arrays und STL-Container durchlaufen kann.

struct MyOtherObject { int myOtherInt; REFLECT(MyOtherObject, myOtherInt) };
struct MyObject
{
    int myInt;
    std::string myString;
    MyOtherObject myOtherObject;
    std::vector<int> myIntCollection;

    REFLECT(MyObject, myInt, myString, myOtherObject, myIntCollection)
};

int main()
{
    MyObject myObject = {};
    std::cout << "Enter MyObject:" << std::endl;
    std::cin >> Json::in(myObject);
    std::cout << std::endl << std::endl << "You entered:" << std::endl;
    std::cout << Json::pretty(myObject);
}

Der obige Ablauf könnte so aussehen...

Enter MyObject:
{
  "myInt": 1337, "myString": "stringy", "myIntCollection": [2,4,6],
  "myOtherObject": {
    "myOtherInt": 9001
  }
}

You entered:
{
  "myInt": 1337,
  "myString": "stringy",
  "myOtherObject": {
    "myOtherInt": 9001
  },
  "myIntCollection": [ 2, 4, 6 ]
}

Siehe auch...

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X