Serialisieren einer Structure - Frage zum Umgang mit "universellen" Pointern



  • Hallo,

    in meinem Programm nutze ich einen std::vector aus einer selbst definierten structure, welche verschiedene Datentypen beinhaltet.

    Ich möchte der Structure nun eine serialisierungsmethode geben, die alle inernen Daten und jeweils einen Bezeichner für diese Daten ausgibt. Dafür suche ich nach einer für einen Anfänger einfach nachvollziehbaren Methode, und habe mir folgendes gedacht:

    Ich lege in der betreffenden Structure eine std::map<> an. Beispielhaft sind hier auch drei Variablen in dieser Structure aufgeführt.

    std::map<std::string, void*>  var_map;
    int a;
    std::string b;
    vector<int> c;
    

    Diese Map soll nun Pointer auf die in der Structure befindlichen Daten gemeinsam mit deren internen Variablenbezeichnungen speichern. Im Konstruktor wird diese Map also mit den Pointern auf diese Daten entsprechend gefüllt.

    var_map["bezeichnung_a"]	=&a;
    var_map["bezeichnung_b"]	=&b;
    var_map["bezeichnung_c"]	=&c;
    

    Die Serialisierungsmethode soll nun durch die Map gehen, und für die Variablentypen jeweils eine Ausgabefunktion aufrufen, z.B. für Integer eine Methode, die Integer in String umwandelt, und vorher noch den Variablennamen ausgibt. Die Frage ist nun: Wie finde ich beim abarbeiten der Map heraus, welcher Datentyp sich hinter dem dort abgelegten Pointer befindet?

    Konkret, ich rufe folgendes auf:

    pointer = var_map["bezeichnung_b"];
    

    Dann müsste ich ja einen Pointer auf ein std::string Objekt bekommen.

    1. Von welchem Typ müsste "pointer" in diesem Beispiel sein? (Es könnte ja auch ein andere Pointer in der Map stehen, z.B. auf einen int-typ.)

    2. Wie kann ich zur Laufzeit herausfinden, daß es sich bei (var_map["bezeichnung_b"]) um ein std::string Objekt handelt?

    3. Und natürlich: Wie kann ich aus "pointer" wieder einen std::string machen, wenn ich weiß, daß sich ein std::string dahinter verbirgt?

    4. Gibt es vielleicht sowieso schon sowas ähnliches wie diese "manuell konstruierte" Map innerhalb der Structure, so daß man diese gar nicht erst erzeugen müsste? Also: Kann man innerhalb einer Structure (unkompliziert) an die Pointer aller dort abgelegten Daten bekommen?

    Danke für eure Hilfe!



  • Der Ansatz ist unbrauchbar. Wie du schon festgestellt hast, gehen die Typinformationen verloren. Sieh dir mal boost::serialization an.



  • Danke für die Antwort. Gibt es - mal abgesehen von der Nutzung von Bibliotheken - eine Methode, um es so ähnlich zu machen, wie ich es mir gedacht habe?

    Man könnte z.B. für die verschiedenen Typen in der Structure jeweils einfach eine extra-map mit pointern auf ebendiese Typen anlegen. Das wäre die etwas unübersichtliche Variante. Geht es eleganter? Also gibt es in C++ vielleicht sowas wie ein "Pointer plus Typ"-Structure? (zur Not könnte man sich das ja auch selbst definieren) Am Ende läuft es vermutlich darauf hinaus, daß ich mir eine kleine Klasse für sowas schreibe, die Pointer/Typen/Bezeichnung-Datensätze in eine Map aufnimmt und die jeweiligen Serialisierungsmethoden für die Typen bereitstellt...



  • Wie gesagt, sieh dir mal boost an. Du musst es ja nicht verwenden.

    Im Prinzip gibt es da für jeden Grunddatentyp (sowie für strings, stl container u.ä.) eine Überladung. Die zu serialisierende Klasse muss dann eine .serialize() Methode verpasst bekommen, die jeden Member in ein sog. "Archive" schreibt. Das Archive ist im Prinzip auch nur eine Art Stream, für das wie bei z.B. std::cout die Operatoren überladen wurden.

    Genaueres findest du dann in der Boost-Doku.

    Edit: was natürlich auch ginge, wäre mit ein bisschen Makro-Zauberei Reflection. (In diesem Fall hättest du dann deine Map.) Allerdings ist das schon ziemlich pervers und außerdem nicht gerade performant.



  • Hallo maugli,

    einen Automatismus, der die die gewünschte (Meta-)Information über die Member einer strcut oder class erzeugt, gibt es in C++ nicht.

    Ich würde Dir empfehlen für Deine struct Streaming-Operatoren zu implementieren. Also Deine struct sei:

    struct Maugli
    {
        int a;
        std::string b;
        std::vector< int > c;
    };
    

    .. dann fügt man die beiden Streaming-Funktionen für das Abspeichern und Laden hinzu - in Form von friend-Funktionen hast Du vollen Zugriff auf die Member der struct, auch wenn man sie später vielleicht private macht.

    struct Maugli
    {
        // --   Schreiben
        friend std::ostream& operator<<( std::ostream& out, const Maugli& m );
        // --   Lesen
        friend std::istream& operator>>( std::istream& in, Maugli& m );
    
        int a;
        std::string b;
        std::vector< int > c;
    };
    

    Anschließend kannst Du Instanzen dieser struct - also Variablen vom Typ Maugli schreiben und lesen wie auch einzelne int-Werte - etwa so:

    int main()
    {
        using namespace std;
        {   // --   Schreiben
            Maugli m;
            m.c.push_back( 42 );
            m.c.push_back( 43 );
            m.b = "mal sehen";
            ofstream datei("datei.txt");
            datei << m;
        }
        {   // -- Lesen
            Maugli mm;
            ifstream datei("datei.txt");
            if( datei >> mm )
            {
                cout << "Sollte = 43 sein: " << mm.c[1] << endl;
            }
        }
        cin.get();
        return 0;
    }
    

    und dann fehlt "nur noch" die Implementierung der Streaming-Operatoren - hier alles zusammen:

    #include <algorithm> // copy
    #include <iostream>
    #include <fstream>
    #include <iterator> // ostream_iterator
    #include <string>
    #include <vector>
    #include <limits> // numeric_limits
    
    struct Maugli
    {
        friend std::ostream& operator<<( std::ostream& out, const Maugli& m );
        friend std::istream& operator>>( std::istream& in, Maugli& m );
    
        int a;
        std::string b;
        std::vector< int > c;
    };
    
    // --   Schreiben
    std::ostream& operator<<( std::ostream& out, const Maugli& m )
    {
        out << "a=" << m.a << "\n";
    
        out << "b=" << m.b << "\n";
    
        out << "c: anzahl=" << m.c.size() << "\n";
        copy( m.c.begin(), m.c.end(), std::ostream_iterator< int >( out, "\n" ) );
        return out;
    }
    // --   Lesen
    std::istream& operator>>( std::istream& in, Maugli& m )
    {
        std::string token;
        if( getline( in >> std::ws, token, '=' ) && token != "a" )
            in.setstate( std::ios_base::failbit );
        in >> m.a;
    
        if( getline( in >> std::ws, token, '=' ) && token != "b" )
            in.setstate( std::ios_base::failbit );
        getline( in, m.b );
    
        if( getline( in >> std::ws, token, ':' ) && token != "c" )
            in.setstate( std::ios_base::failbit );
        std::size_t c_anzahl;
        in.ignore( std::numeric_limits< std::streamsize >::max(), '=' ) >> c_anzahl;
        std::vector< int > c;
        c.reserve( c_anzahl );
        for( int c_element; c_anzahl > 0 && in >> c_element; --c_anzahl )
            c.push_back( c_element );
        if( in )
            swap( c, m.c );
    
        return in;
    }
    
    int main()
    {
        using namespace std;
        {   // --   Schreiben
            Maugli m;
            m.c.push_back( 42 );
            m.c.push_back( 43 );
            m.b = "mal sehen";
            ofstream datei("datei.txt");
            datei << m;
        }
        {   // -- Lesen
            Maugli mm;
            ifstream datei("datei.txt");
            if( datei >> mm )
            {
                cout << "Sollte = 43 sein: " << mm.c[1] << endl;
            }
        }
        cin.get();
        return 0;
    }
    

    Falls Du Fragen hast, so nur zu - und möglichst konkret.

    Gruß
    Werner



  • Hallo Werner und 31415.. und danke für eure Lösungen und Infos. @Werner: Ich verstehe schon was Du da machst, hatte mir den Mechanismus aber doch ein bischen anders vorgestellt. So wie Du das machst muss jetzt ja für jeden Member in der Struct ein eigener Programmteil in die Implemtierung zur Aus- & Eingabe geschrieben werden - wenn man später in der Struct einen Member in der struct ergänzen will ist das nicht so optimal. Ich fänd es besser, wenn man später einfach neue andere Variablen in der struct ergänzen kann.

    Ich habe inzwischen eine andere Idee umgesetzt, es funktioniert nur leider noch nicht... Kannst Du Dir das mal kurz ansehen? Offenbar zeigen die Pointer meiner Map ins Leere (Abstürze).

    Die Idee ist, folgende Klasse *innerhalb* der zu serialisierenden Structure zu instantiieren. Somit sollen Pointer auf die zu serialisierenden Variablen der Structure an diese Klasse übergeben werden.

    class	cSerialize
    {private:		std::map<unsigned int, std::string>			map_iterator;
    				std::map<std::string, void*>				map_pointer;
    				std::map<std::string, unsigned int>			map_type;
    
    			unsigned int next;
    
    public:		cSerialize::cSerialize() {it=0;}
    			std::string serialize(void);
    			void add(std::string name, unsigned int typ, void* pointer);
    };
    

    In meiner Struct übergebe ich Zeiger auf deren eigene Variablen - und zwar im Konstruktor - an eine Instanz dieser Klasse innerhalb der structure. Das sieht dann so aus:

    struct Maugli
    {	cSerialize serializierer;
    
    	int a;
    	std::string b;
    	std::vector< int > c;
    
    Maugli::Maugli()
    		{serializierer.add("name_a", 0, &a);
    		serializierer.add("name_b", 1, &b);
    		serializierer.add("name_c", 2, &c);
    		}
    
    };
    

    Der Variablentyp wird also intern zusammen mit dem Pointer und einem Variablennamen-Text abgelegt. Das funktioniert soweit auch alles. Die Funktion "serialize()" der Klasse cSerialize sieht nun (verkürzt auf den Datentyp int) so aus:

    std::string output;
    
    for (unsigned int k=0; k<next; ++k)
    {std::string var_name=map_iterator[k]; 		// besorgt aus der ersten Map den internen Variablennamen 
    
    if (map_type[var_name]==0) 			// falls es sich um einen *int handelt..
    
    output+="<"+var_name+">" + IntToString	(*reinterpret_cast<int*>(map_pointer[var_name]));  // wird der Pointer in der Pointer-Map nach *int gecastet, dereferenziert, und zusammen mit dem Variablennamen abgelegt. 
    
    // [...] 
    
    }
    

    Was genau stimmt daran nicht? Die Typinfos und Variablennamen werden korrekt abgerufen, aber der Pointer scheint irgendwie nicht in Ordnung zu sein.

    (Ich weiß, daß es nicht ganz sauber ist, void* zu verwenden. Möglich wäre auch eine Map für jeden Datentyp innerhalb cSerialize, und die Überladung deren add()-Funktion. Ich habe das Probeweise auch mal ausprobiert, bekomme aber denselben Fehler.)

    Danke Dir (und den anderen) für jede Hilfe!



  • Du willst es also unbedingt auf die Java-eske Variante für faule - na gut, du wurdest gewarnt.

    Folgendes geht beinahe ohne viel weitere Arbeit - allerdings hat dies einige Nachteile gegenüber boost:
    - Langsamer
    - Um Vererbung hab ich mich nicht gekümmert, möglicherweise gibts da Probleme
    - Byte-Order systemabhängig
    - Undurchsichtig (Makros)
    - Definiert Namen in der Klasse, die quasi unsichtbar sind
    - Zusätzlicher Speicherverbrauch pro Member je Objekt von 4 oder 8 Byte, je nach System (Memberfunktionszeiger)

    #include <vector>
    #include <type_traits>
    
    template <typename T>
    void serialize(std::ostream& os, T const& value, std::integral_constant<bool, false>)
    {
    	value.serialize(os);
    }
    
    template <typename T>
    void serialize(std::ostream& os, T value, std::integral_constant<bool, true>)
    {
    	os.write(reinterpret_cast<char const*>(&value), sizeof value);
    }
    
    template <typename T>
    void serialize(std::ostream& os, T const& value)
    {
    	serialize(os, value, std::integral_constant<bool, std::is_fundamental<T>::value>());
    }
    
    template <typename ClassType>
    struct serialized_field
    {
    	virtual void serialize(ClassType const& obj, std::ostream& os) const = 0;
    };
    
    template <typename FieldType, typename ClassType>
    struct concrete_serialized_field : serialized_field<ClassType>
    {
    	FieldType const ClassType::* const value;
    
    	concrete_serialized_field(FieldType const ClassType::* value)
    		: value(value)
    	{}
    
    	virtual void serialize(ClassType const& obj, std::ostream& os) const
    	{
    		::serialize(os, obj.*value);
    	}
    };
    
    #define SERIALIZEABLE(name) \
    	std::vector<serialized_field<name> const*> serialized_fields; \
    	typedef name this_type; \
    	friend void serialize<name>(std::ostream& os, name const& thisval, std::integral_constant<bool, false>); \
    	void serialize(std::ostream& os) const \
    	{ \
    		for(auto iter = serialized_fields.begin(); iter != serialized_fields.end(); ++iter) \
    			(*iter)->serialize(*this, os); \
    	}
    
    #define SERIALIZED_FIELD(type, name) type name; \
    	static serialized_field<this_type> const& field_##name##_wrapper() \
    	{ \
    		static concrete_serialized_field<type, this_type> field(&this_type::name); \
    		return field; \
    	}
    
    #define SERIALIZED_FIELD_INIT(name) serialized_fields.push_back(&field_##name##_wrapper());
    

    Verwendet man dann so:

    class foo
    {
    	SERIALIZEABLE(foo)
    
    	SERIALIZED_FIELD(int, i)
    	SERIALIZED_FIELD(char, c)
    
    public:
    	foo()
    		: i(42)
    		, c('P')
    	{
    		SERIALIZED_FIELD_INIT(i)
    		SERIALIZED_FIELD_INIT(c)
    	}
    };
    
    int main()
    {
    	foo f;
    	serialize(std::cout, f);
    	serialize(std::cout, 666);
    	return 0;
    }
    

    Achja, ums Einlesen hab ich mich nicht gekümmert. Das darfst du machen, damit du auch ein bisschen darunter leidest, falls du das verwenden willst. 😉
    Das SERIALIZED_FIELD_INIT-Makro ist lässt sich noch wegbringen, falls dein Compiler ausreichenden C++11 Support hat - meiner kanns nicht.

    Edit: Und falls du komplexere Datentypen schreiben möchtest, die du aber nicht verändern kannst (z.B. std::string), dann musst du eine Funktion dafür definieren, z.B. so:

    void serialize(std::ostream& os, std::string const& s, std::integral_constant<bool, false>)
    {
    	std::string::size_type size = s.size();
    	::serialize(os, size);
    	os.write(s.c_str(), size);
    }
    


  • Hey 31415,

    danke für Dein Codebeispiel. Es ist immer ein bischen schwierig - und bitte nimms mir nicht übel daß ich das sage, wenn man es auf Weg A versucht, und dann 4 ganz andere Wege vorgeschlagen bekommt 🙂 Und man lernt sicherlich auch was dabei, aber vor allen Dingen eben, wie andere es gelöst hätten 😃 Was ja auch durchaus die bessere Lösung sein mag, das bezweifle ich gar nicht. Aber davon mal abgesehen:
    Kannst Du bitte mal kurz über meinen Code drübersehen und sagen, wo der Denkfehler ist, also warum dort die in der Map abgelegten Pointer nicht in Ordnung sind? Vielleicht hast Du ja eine Idee.

    Danke Dir!
    maugli 🙂



  • ...hat sich gerade erledigt. Der Denkfehler war, daß die Map ja gefüllt wird, wenn der Konstruktor der Structure aufgerufen wird. Sobald man so eine Structure aber umkopiert wird die Map automatisch mitkopiert - zeigt aber dann noch auf das ursprüngliche Objekt, hat also mit dem Objekt nichts zu tun, und falls es nicht mehr da ist zeigt sie ins leere.

    Ich habs jetzt so gelöst, daß man bevor man "serialize" aufruft in der Structure eine Methode aufruft, die direkt davor die aktuellen Pointer der Elemente in die Map übernimmt.


Anmelden zum Antworten