Wie serialisiert man ein Objekt in C ++?

83

Ich habe eine kleine Hierarchie von Objekten, die ich serialisieren und über eine Socket-Verbindung übertragen muss. Ich muss das Objekt sowohl serialisieren als auch basierend auf dem Typ deserialisieren. Gibt es eine einfache Möglichkeit, dies in C ++ zu tun (wie es in Java der Fall ist)?

Gibt es Online-Codebeispiele oder Tutorials für die C ++ - Serialisierung?

EDIT: Um ganz klar zu sein, suche ich nach Methoden zum Konvertieren eines Objekts in ein Array von Bytes und dann zurück in ein Objekt. Ich kann mit der Steckdosenübertragung umgehen.

Bill die Eidechse
quelle
3
Schauen Sie sich google :: protobuf an , es ist eine sehr starke und schnelle Bibliothek für die binäre Serialisierung. Wir haben es erfolgreich mit boost :: asio etc. eingesetzt
Ketan
Schauen Sie sich [STLPLUS] [1], lib mit Persistenzimplementierung an. [1]: stlplus.sourceforge.net
lsalamon
4
Die Antworten erklären nicht wirklich, wie man serialisiert. Einer bietet die Boost-Serialisierungsbibliothek an, der andere erklärt Fallstricke in einer naiven Implementierung. Da dies eine C ++ - FAQ- Frage ist, kann jemand sie tatsächlich beantworten?
anonym

Antworten:

54

Wenn ich über Serialisierung spreche, fällt mir die Boost-Serialisierungs-API ein . Für die Übertragung der serialisierten Daten über das Internet würde ich entweder Berkeley-Sockets oder die Asio-Bibliothek verwenden .

Bearbeiten:
Wenn Sie Ihre Objekte in ein Byte-Array serialisieren möchten, können Sie den Boost-Serializer folgendermaßen verwenden (entnommen aus der Tutorial-Site):

#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
class gps_position
{
private:
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & degrees;
        ar & minutes;
        ar & seconds;
    }
    int degrees;
    int minutes;
    float seconds;

public:
    gps_position(){};
    gps_position(int d, int m, float s) :
    degrees(d), minutes(m), seconds(s)
    {}
};

Die eigentliche Serialisierung ist dann ziemlich einfach:

#include <fstream>
std::ofstream ofs("filename.dat", std::ios::binary);

    // create class instance
    const gps_position g(35, 59, 24.567f);

    // save data to archive
    {
        boost::archive::binary_oarchive oa(ofs);
        // write class instance to archive
        oa << g;
        // archive and stream closed when destructors are called
    }

Die Deserialisierung funktioniert analog.

Es gibt auch Mechanismen, mit denen Sie die Serialisierung von Zeigern handhaben können (komplexe Datenstrukturen wie Locken usw. sind kein Problem), abgeleitete Klassen und Sie können zwischen Binär- und Textserialisierung wählen. Außerdem werden alle STL-Container sofort unterstützt.

Newgre
quelle
Dies ist eine C ++ - Frage, warum die Klasse gps_position den Operator << überlastet. Es ist keine Freundfunktion definiert
Vicente Bolea
Beachten Sie die "Friend Class Boost :: Serialization :: Access". Dies ermöglicht den Klassenmitgliedern den Zugriff auf die Funktionen der Serialisierungsbibliothek, auch wenn sie privat sind.
Robert Ramey
13

In einigen Fällen können Sie beim Umgang mit einfachen Typen Folgendes tun:

object o;
socket.write(&o, sizeof(o));

Das ist als Proof-of-Concept oder First-Draft in Ordnung, damit andere Mitglieder Ihres Teams weiter an anderen Teilen arbeiten können.

Aber früher oder später, normalerweise früher , werden Sie verletzt!

Sie stoßen auf Probleme mit:

  • Virtuelle Zeigertabellen werden beschädigt.
  • Zeiger (auf Daten / Mitglieder / Funktionen) werden beschädigt.
  • Unterschiede in der Polsterung / Ausrichtung auf verschiedenen Maschinen.
  • Probleme bei der Bestellung von Big / Little-Endian-Bytes.
  • Variationen in der Implementierung von float / double.

(Außerdem müssen Sie wissen, in was Sie auf der Empfangsseite auspacken.)

Sie können dies verbessern, indem Sie für jede Klasse Ihre eigenen Marshalling- / Unmarshalling-Methoden entwickeln. (Idealerweise virtuell, damit sie in Unterklassen erweitert werden können.) Mit ein paar einfachen Makros können Sie verschiedene Basistypen recht schnell in einer großen / kleinen endian-neutralen Reihenfolge schreiben.

Diese Art von Grunzarbeit ist jedoch viel besser und einfacher über die Serialisierungsbibliothek von boost zu erledigen .

Mr.Ree
quelle
Daran habe ich gedacht. Da ich jedoch in einen Netzwerk-Stream serialisieren möchte, funktioniert dies überhaupt nicht. Höchstens wegen der Endianness und der unterschiedlichen Plattformen. Aber ich wusste nicht, dass es virtuelle Zeiger beschädigt. Danke =)
Atmocreations
2

Es gibt ein generisches Muster, mit dem Sie Objekte serialisieren können. Das grundlegende Grundelement sind diese beiden Funktionen, die Sie von Iteratoren lesen und schreiben können:

template <class OutputCharIterator>
void putByte(char byte, OutputCharIterator &&it)
{
    *it = byte;
    ++it;
}


template <class InputCharIterator>
char getByte(InputCharIterator &&it, InputCharIterator &&end)
{
    if (it == end)
    {
        throw std::runtime_error{"Unexpected end of stream."};
    }

    char byte = *it;
    ++it;
    return byte;
}

Dann folgen Serialisierungs- und Deserialisierungsfunktionen dem Muster:

template <class OutputCharIterator>
void serialize(const YourType &obj, OutputCharIterator &&it)
{
    // Call putbyte or other serialize overloads.
}

template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
    // Call getByte or other deserialize overloads.
}

Für Klassen können Sie das Friend-Funktionsmuster verwenden, damit die Überladung mithilfe von ADL gefunden werden kann:

class Foo
{
    int internal1, internal2;

    // So it can be found using ADL and it accesses private parts.
    template <class OutputCharIterator>
    friend void serialize(const Foo &obj, OutputCharIterator &&it)
    {
        // Call putByte or other serialize overloads.
    }

    // Deserialize similar.
};

Die in Ihrem Programm können Sie serialisieren und in eine Datei wie diese objektivieren:

std::ofstream file("savestate.bin");
serialize(yourObject, std::ostreambuf_iterator<char>(file));

Dann lies:

std::ifstream file("savestate.bin");
deserialize(yourObject, std::istreamBuf_iterator<char>(file), std::istreamBuf_iterator<char>());

Meine alte Antwort hier:

Serialisierung bedeutet, dass Sie Ihr Objekt in Binärdaten umwandeln. Während Deserialisierung bedeutet, ein Objekt aus den Daten neu zu erstellen.

Bei der Serialisierung werden Bytes in einen uint8_tVektor verschoben. Beim Unserialisieren lesen Sie Bytes aus einem uint8_tVektor.

Es gibt sicherlich Muster, die Sie beim Serialisieren von Inhalten verwenden können.

Jede serialisierbare Klasse sollte eine serialize(std::vector<uint8_t> &binaryData)oder eine ähnliche signierte Funktion haben, die ihre binäre Darstellung in den bereitgestellten Vektor schreibt. Dann kann diese Funktion diesen Vektor an die Serialisierungsfunktionen des Mitglieds weitergeben, damit diese auch ihre Daten darin schreiben können.

Da die Datendarstellung auf verschiedenen Architekturen unterschiedlich sein kann. Sie müssen ein Schema finden, wie die Daten dargestellt werden.

Beginnen wir mit den Grundlagen:

Ganzzahlige Daten serialisieren

Schreiben Sie einfach die Bytes in Little-Endian-Reihenfolge. Oder verwenden Sie die Varint-Darstellung, wenn es auf die Größe ankommt.

Serialisierung in Little-Endian-Reihenfolge:

data.push_back(integer32 & 0xFF);
data.push_back((integer32 >> 8) & 0xFF);
data.push_back((integer32 >> 16) & 0xFF);
data.push_back((integer32 >> 24) & 0xFF);

Deserialisierung aus Little Endian Order:

integer32 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);

Gleitkommadaten serialisieren

Soweit ich weiß, hat der IEEE 754 hier ein Monopol. Ich kenne keine Mainstream-Architektur, die etwas anderes für Floats verwenden würde. Das einzige, was anders sein kann, ist die Bytereihenfolge. Einige Architekturen verwenden Little Endian, andere die Big Endian-Bytereihenfolge. Dies bedeutet, dass Sie vorsichtig sein müssen, in welcher Reihenfolge die Bytes auf der Empfangsseite laut werden. Ein weiterer Unterschied kann die Handhabung der Denormal- und Unendlichkeits- und NAN-Werte sein. Solange Sie diese Werte vermeiden, sollten Sie in Ordnung sein.

Serialisierung:

uint8_t mem[8];
memcpy(mem, doubleValue, 8);
data.push_back(mem[0]);
data.push_back(mem[1]);
...

Deserialisierung macht es rückwärts. Beachten Sie die Bytereihenfolge Ihrer Architektur!

Strings serialisieren

Zuerst müssen Sie sich auf eine Kodierung einigen. UTF-8 ist üblich. Speichern Sie es dann als Länge vorangestellt: Zuerst speichern Sie die Länge der Zeichenfolge mit einer oben erwähnten Methode, dann schreiben Sie die Zeichenfolge Byte für Byte.

Arrays serialisieren.

Sie sind die gleichen wie eine Saite. Sie serialisieren zuerst eine Ganzzahl, die die Größe des Arrays darstellt, und serialisieren dann jedes Objekt darin.

Ganze Objekte serialisieren

Wie ich bereits sagte, sollten sie eine serializeMethode haben, die einem Vektor Inhalt hinzufügt. Um ein Objekt unserialisieren zu können, sollte es einen Konstruktor haben, der Byte-Streams verwendet. Es kann istreamein Referenzzeiger sein, aber im einfachsten Fall kann es nur ein Referenzzeiger sein uint8_t. Der Konstruktor liest die gewünschten Bytes aus dem Stream und richtet die Felder im Objekt ein. Wenn das System gut entworfen ist und die Felder in der Reihenfolge der Objektfelder serialisiert, können Sie den Stream einfach in einer Initialisierungsliste an die Konstruktoren des Felds übergeben und sie in der richtigen Reihenfolge deserialisieren lassen.

Objektdiagramme serialisieren

Zuerst müssen Sie sicherstellen, dass diese Objekte wirklich etwas sind, das Sie serialisieren möchten. Sie müssen sie nicht serialisieren, wenn Instanzen dieser Objekte auf dem Ziel vorhanden sind.

Jetzt haben Sie herausgefunden, dass Sie das Objekt, auf das ein Zeiger zeigt, serialisieren müssen. Das Problem der Zeiger, dass sie nur in dem Programm gültig sind, das sie verwendet. Sie können Zeiger nicht serialisieren, Sie sollten sie nicht mehr in Objekten verwenden. Erstellen Sie stattdessen Objektpools. Dieser Objektpool ist im Grunde ein dynamisches Array, das "Boxen" enthält. Diese Felder haben eine Referenzanzahl. Ein Referenzzähler ungleich Null zeigt ein lebendes Objekt an, Null zeigt einen leeren Steckplatz an. Anschließend erstellen Sie einen intelligenten Zeiger, der dem shared_ptr ähnelt und nicht den Zeiger auf das Objekt, sondern den Index im Array speichert. Sie müssen sich auch auf einen Index einigen, der den Nullzeiger kennzeichnet, z. -1.

Grundsätzlich haben wir hier die Zeiger durch Array-Indizes ersetzt. Jetzt können Sie beim Serialisieren diesen Array-Index wie gewohnt serialisieren. Sie müssen sich keine Gedanken darüber machen, wo sich das Objekt auf dem Zielsystem im Speicher befindet. Stellen Sie einfach sicher, dass sie auch denselben Objektpool haben.

Wir müssen also die Objektpools serialisieren. Aber welche? Wenn Sie ein Objektdiagramm serialisieren, serialisieren Sie nicht nur ein Objekt, sondern ein gesamtes System. Dies bedeutet, dass die Serialisierung des Systems nicht von Teilen des Systems ausgehen sollte. Diese Objekte sollten sich nicht um den Rest des Systems kümmern, sie müssen nur die Array-Indizes serialisieren und das wars. Sie sollten über eine System-Serializer-Routine verfügen, die die Serialisierung des Systems koordiniert, die relevanten Objektpools durchläuft und alle serialisiert.

Auf der Empfangsseite werden alle Arrays und die darin enthaltenen Objekte deserialisiert, wodurch der gewünschte Objektgraph neu erstellt wird.

Funktionszeiger serialisieren

Speichern Sie keine Zeiger im Objekt. Haben Sie ein statisches Array, das die Zeiger auf diese Funktionen enthält, und speichern Sie den Index im Objekt.

Da diese Programme in beiden Programmen in die Regale kompiliert sind, sollte es funktionieren, nur den Index zu verwenden.

Serialisierung polymorpher Typen

Da ich sagte, Sie sollten Zeiger in serialisierbaren Typen vermeiden und stattdessen Array-Indizes verwenden, kann Polymorphismus einfach nicht funktionieren, da Zeiger erforderlich sind.

Sie müssen dies mit Typ-Tags und Gewerkschaften umgehen.

Versionierung

Darüber hinaus. Möglicherweise möchten Sie, dass verschiedene Versionen der Software zusammenarbeiten.

In diesem Fall sollte jedes Objekt zu Beginn seiner Serialisierung eine Versionsnummer schreiben, um die Version anzugeben.

Wenn Sie das Objekt auf der anderen Seite laden, können neuere Objekte möglicherweise die älteren Darstellungen verarbeiten, aber die älteren können die neueren nicht verarbeiten, sodass sie eine Ausnahme auslösen sollten.

Jedes Mal, wenn sich etwas ändert, sollten Sie die Versionsnummer erhöhen.


Zum Abschluss kann die Serialisierung komplex sein. Glücklicherweise müssen Sie nicht alles in Ihrem Programm serialisieren. Meistens werden nur die Protokollnachrichten serialisiert, bei denen es sich häufig um einfache alte Strukturen handelt. Sie brauchen also die komplexen Tricks, die ich oben erwähnt habe, nicht zu oft.

Calmarius
quelle
Danke dir. Diese Antwort enthält einen guten Überblick über die Konzepte, die für die Serialisierung strukturierter Daten in C ++ relevant sind.
Sean
0

Zum Lernen habe ich einen einfachen C ++ 11-Serializer geschrieben. Ich hatte verschiedene andere schwerere Angebote ausprobiert, wollte aber etwas, das ich tatsächlich verstehen konnte, wenn es schief ging oder nicht mit dem neuesten g ++ kompiliert werden konnte (was für mich mit Cereal passiert ist; eine wirklich schöne Bibliothek, aber komplex und ich konnte nicht groken die Fehler, die der Compiler beim Upgrade gemeldet hat.) Wie auch immer, es ist nur ein Header und verarbeitet POD-Typen, Container, Maps usw. Keine Versionierung und es werden nur Dateien aus demselben Bogen geladen, in dem es gespeichert wurde.

https://github.com/goblinhack/simple-c-plus-plus-serializer

Anwendungsbeispiel:

#include "c_plus_plus_serializer.h"

static void serialize (std::ofstream out)
{
    char a = 42;
    unsigned short b = 65535;
    int c = 123456;
    float d = std::numeric_limits<float>::max();
    double e = std::numeric_limits<double>::max();
    std::string f("hello");

    out << bits(a) << bits(b) << bits(c) << bits(d);
    out << bits(e) << bits(f);
}

static void deserialize (std::ifstream in)
{
    char a;
    unsigned short b;
    int c;
    float d;
    double e;
    std::string f;

    in >> bits(a) >> bits(b) >> bits(c) >> bits(d);
    in >> bits(e) >> bits(f);
}

Neil McGill
quelle