Deserialisierung von verknüpften Objekten



  • Tag zusammen,

    folgendes Problem, das mir momentan Kopfschmerzen bereitet:

    Ich habe zwei Klassen, nennen wir sie "A" und "B":

    class A { string id; };
    class B { A* a; };
    

    B hat also einen Member auf A. Soweit kein Problem, doch lade ich Objekte dieser Klassen nun aus einer Datei (YAML-Syntax, spielt hier aber keine Rolle). Dabei werden zunächst alle A-Objekte geladen und später die B-Objekte, die auch später dazugeladen werden können.

    Nun gibt's ja das Problem, dass man in einer Datei logischerweise keine Zeiger unterbringen kann, daher sichere ich die A-ID des A-Objekts, das ich für B referenzieren möchte. Die Relation muss also nach dem Laden hergestellt werden. Und hier ist mein Problem: Wie löst man so etwas möglichst elegant?

    Momentan läuft es so, dass es eine Klasse YAMLDriver gibt, die die Methoden zum Laden von A- und B-Objekten beherbergt. LoadB() weiß aber natürlich dadurch von keinen A-Objekten, d.h. das Setzen des Pointers ist hier gar nicht möglich. Daher ruft LoadB() einen Callback auf, der ihm anhand der A-ID einen Pointer liefert.

    Das funktioniert sehr gut, aber ich bezweifle, dass dies schön ist. Für Tipps jeglicher Art wäre ich sehr dankbar, der Kopf qualmt. 😉



  • Ich hätte das aber ähnlich gemacht, wobei mir nicht ganz klar ist, wie dein Callback funktioniert. Du musst ja irgendwo speichern, welches Objekte zu einem anderen eine Beziehung hat, um die Zeiger korrekt zu setzen. Ich finde, man könnte die Setzung der Relationen sogar in eine gesonderte Funktion createRelationShips() auslagern, sodass die Ladereihenfolge der einstigen Objekte egal wird, aber dadurch hat man natürlich nicht eine Funktion pro Klasse.

    Ich weiß aber nicht, ob boost da nicht irgendwas Cooles hat.



  • Die Signatur des Callbacks ist folgendermaßen:

    A* GetA( const std::string& id )
    

    Du musst ja irgendwo speichern, welches Objekte zu einem anderen eine Beziehung hat

    Das gibt der Benutzer ja eben durch die Dateien vor, die da geladen werden. 🙂 Zusätzlich ruft die Instanz die Lade-Routine auf, die auch den Callback abfängt und die A-Objekte selbst besitzt.

    Hier mal ein Beispiel zweier solcher Dateien, wie sie ähnlich bei mir aussehen:

    A.yml:

    Id: "cpp.de"
    

    B.yml:

    Id: "wasauchimmer"
    A: "cpp.de"
    

    Boost hat generell Boost.Serialization, was für meine Belange aber 1. Overkill und 2. eigentlich nicht nötig ist, da die Anwender selber die YAML-Dateien schreiben, d.h. Serialisierung macht keinen Sinn (nur Deserialisierung).



  • Wenn du sowieso alles in deiner Loader-Klasse untergebracht hast und diesen für deine Anwendung maßgeschneidert angefertigt hast, dann sollte die auch die Zuordnungen wiederherstellen. Eine Möglichkeit wäre es, alle geladenen A-Objekte in einer map<string,A*> zu speichern und beim Laden der B-Objekte aus dieser map das zugehörige Objekt herauszusuchen.
    (das setzt natürlich voraus, daß die Objekte in der richtigen Reihenfolge eingelesen werden)



  • Die wichtigsten Stichworte sind schon gefallen. Einen Katalog anlegen vom Typ std::mapstd::string,A*, beim Einlesen der A-Objekte diese in dem Katalog registrieren und beim Lesen der B-Objekte diese mit den bereits bestehenden A-Objekten verlinken.

    Elegant wird's, wenn Du den Katalog an den istream, aus dem Du liest, bindest. Das läuft dann so ähnlich wie in diesem Thread, wo sich auch was im Hintergrund gemerkt wird, um es später wieder zu verwenden. Hier wird es etwas einfacher:

    #include <iostream>
    #include <map>
    #include <sstream>
    
    class A;
    
    namespace detail
    {
        typedef std::map< std::string, A* > catalogA_t;
    
        catalogA_t*& getcatalogA( std::ios_base& ios );
    
        void destroyCatalogA( std::ios_base::event ev, std::ios_base& ios, int /*index*/ )
        {
            if( ev == std::ios_base::erase_event )
            {
                catalogA_t*& cat = getcatalogA( ios );
                delete cat;
                cat = 0;
            }
        }
    
        catalogA_t*& getcatalogA( std::ios_base& ios )
        {
            static int IOS_INDEX_CATALOG_A = std::ios_base::xalloc();
            catalogA_t*& cat = reinterpret_cast< catalogA_t*& >( ios.pword( IOS_INDEX_CATALOG_A ) );
            if( !cat )
            {
                cat = new catalogA_t;
                ios.register_callback( &destroyCatalogA, 0 );
            }
            return cat;
        }
    }
    
    // --   Hier registriert sich das Ziel mit seiner Id 'im Stream'
    void registerA( std::ios_base& ios, const std::string& id, A* a )
    {
        detail::catalogA_t* cat = detail::getcatalogA( ios );
        cat->insert( std::make_pair( id, a ) ); // ggf. Fehlerbehandlung bei doppelten Einträgen
    }
    
    // --   Manipulator zum Auslesen des Links auf ein registriertes A-Objekt
    struct readAPtr
    {
        typedef A* APtr_t;
        explicit readAPtr( APtr_t& a ) : m_a( &a ) {}
        friend std::istream& operator>>( std::istream& in, readAPtr& r )
        {
            std::string id;
            if( in >> id )
            {
                const detail::catalogA_t* cat = detail::getcatalogA( in );
                detail::catalogA_t::const_iterator i = cat->find( id );
                if( i != cat->end() )
                    *(r.m_a) = i->second;
                else
                    in.setstate( std::ios_base::failbit ); // nicht gefunden ist Lesefehler
            }
            return in; 
        }
    private:
        APtr_t* m_a;
    };
    
    // --   Applikation
    class A
    {
    public:
        friend class B;
        // LoadA
        friend std::istream& operator>>( std::istream& in, A& a )
        {
            if( in >> a.id >> a.nr )
            {   // alles korrekt gelesen, dann A registrieren
                registerA( in, a.id, &a );
            }
            return in; 
        }
    private:
        std::string id;
        int nr;
    };
    
    class B
    {
    public:
        int nr() const { return a->nr; } // nur zur Demo
        // LoadB
        friend std::istream& operator>>( std::istream& in, B& b )
        {
            return in >> readAPtr( b.a ); 
        }
    private:
        A* a;
    };
    int main() // Beispiel für Verlinken von B mit A
    {
        using namespace std;
        A a1, a2, a3;
        B b;
        istringstream in("A1 11 A2 42 A3 13   A2");
        if( in >> a1 >> a2 >> a3 >> b )
            cout << "Ok: b.nr()=" << b.nr() << endl;
        return 0;
    }
    

    Der Anwender muss sich jetzt um nichts mehr kümmern. Das passiert vollständig im Hintergrund. Mit Löschen des Streams wird der Katalog auch gelöscht. Falls ein B-Objekt nicht verlinkt werden kann, so geht der Stream auf fail (s.o. istream>>readAPtr).
    Es versteht sich von selbst, dass Du statt des istringstreams auch einen ifstream hernehmen kannst. Der Output dieses kleinen Programms ist

    Ok: b.nr()=42

    Gruß
    Werner



  • Wenn du sowieso alles in deiner Loader-Klasse untergebracht hast und diesen für deine Anwendung maßgeschneidert angefertigt hast, dann sollte die auch die Zuordnungen wiederherstellen. Eine Möglichkeit wäre es, alle geladenen A-Objekte in einer map<string,A*> zu speichern und beim Laden der B-Objekte aus dieser map das zugehörige Objekt herauszusuchen.

    In abgewandelter Form passiert das momentan genau so. Vielleicht gehe ich noch einmal genauer auf den Momentan-Zustand ein, da hab ich mich möglicherweise unglücklich ausgedrückt. 😉

    1. Es gibt die Klassen A und B, die die Daten darstellen.
    2. Es gibt einen Loader (in meinem Fall "YAMLDriver"), der über zwei Methoden LoadA() und LoadB() die jeweiligen Objekte lädt und erzeugt.
    3. Es gibt die Applikation selbst, die diese Objekte verwaltet.

    Das heißt: Möchte ich nun A-Objekte laden, so rufe ich LoadA() über den Loader und sichere die erzeugten Objekte in der Applikation. Danach ist LoadB() an der Reihe. Da aber die A-Objekte außerhalb des Loaders liegen, kann dieser die Beziehungen nicht herstellen, da er ja nix von A-Objekten weiß und muss (so ist es momentan gelöst) nachfragen -- in meinem Fall über das Callback.

    @Werner:
    Zunächst einmal vielen Dank für deine ausführliche Antwort. Das Prinzip gefällt mir sehr, leider gibt's für meinen Fall einige Probleme: Der Katalog, der zum Verknüpfen benutzt wird, findet sich ähnlich auch bei mir (siehe oben), nur dass bei mir die Laderoutinen nicht direkt in den Daten-Klassen verankert sind. Das hat einfach den Grund, weil nicht ausgeschlossen werden kann, dass YAML bereits die Endstation ist. 😉

    Möglich wäre z.B. das Laden der Daten später aus einem anderen Format, aus einem Binary-String (über's Netz z.B.) oder einer Datenbank durchzuführen. Daher hatte ich mich dazu entschlossen, dass Laden der Daten + Erzeugen der tatsächlichen Objekte separat zu kapseln (YAMLDriver), um genau das austauschen zu können.

    Ich überlege nur, ob es elegant ist, dass der Loader über ein Callback Daten anfragt, die er benötigt. Ist es überhaupt sinnig, dass ein Loader so etwas tut? (eigentlich soll der ja auch nur die Daten laden))

    Eine andere Idee, die mir in den Sinn kam ist folgende: Die ID, die die Relation zwischen A und B herstellt ist ja ein String. Nun könnte der Loader die ID als String laden und in ein Objekt sichern. Ist das Laden abgeschlossen, würden außerhalb des Loaders die Pointer gesetzt werden.

    Das hat allerdings zwangsläufig zur Folge, dass es z.B. von B zwei Versionen geben müsste: Eine mit der ID als String, die andere mit Pointer. Das stelle ich mir dann ehrlich gesagt wartungstechnisch auch eher nervig vor.

    Vielen Dank schon einmal für die Antworten. Und sagt Bescheid, wenn etwas unklar ist oder ich hier mit Kanonen auf Spatzen schieße.



  • StefanBo schrieb:

    Wenn du sowieso alles in deiner Loader-Klasse untergebracht hast und diesen für deine Anwendung maßgeschneidert angefertigt hast, dann sollte die auch die Zuordnungen wiederherstellen. Eine Möglichkeit wäre es, alle geladenen A-Objekte in einer map<string,A*> zu speichern und beim Laden der B-Objekte aus dieser map das zugehörige Objekt herauszusuchen.

    In abgewandelter Form passiert das momentan genau so. Vielleicht gehe ich noch einmal genauer auf den Momentan-Zustand ein, da hab ich mich möglicherweise unglücklich ausgedrückt. 😉

    1. Es gibt die Klassen A und B, die die Daten darstellen.
    2. Es gibt einen Loader (in meinem Fall "YAMLDriver"), der über zwei Methoden LoadA() und LoadB() die jeweiligen Objekte lädt und erzeugt.
    3. Es gibt die Applikation selbst, die diese Objekte verwaltet.

    Das heißt: Möchte ich nun A-Objekte laden, so rufe ich LoadA() über den Loader und sichere die erzeugten Objekte in der Applikation. Danach ist LoadB() an der Reihe. Da aber die A-Objekte außerhalb des Loaders liegen, kann dieser die Beziehungen nicht herstellen, da er ja nix von A-Objekten weiß und muss (so ist es momentan gelöst) nachfragen -- in meinem Fall über das Callback.

    Verwendest du denn unterschiedliche Loader für A- und B-Objekte? Wenn nicht, spricht doch nichts dagegen, wenn der Loader vor der Rückgabe an die Applikation (in LoadA()) einen Zeiger auf das frisch erzeugte Objekt speichert.



  • Die Methoden LoadA() und LoadB() sind in einer Klasse. Deine Idee wäre daher in der Tat umsetzbar, wobei es ein großer Haufen an Objekten ist, d.h. die würden evtl. vor sich hin vegetieren. Mir kann man's aber auch gar nicht recht machen, was? 😉


Anmelden zum Antworten