Come serializzi un oggetto in C ++?


87

Ho una piccola gerarchia di oggetti che devo serializzare e trasmettere tramite una connessione socket. Devo serializzare l'oggetto, quindi deserializzarlo in base al tipo. C'è un modo semplice per farlo in C ++ (come in Java)?

Sono disponibili tutorial o esempi di codice in linea di serializzazione C ++?

EDIT: Giusto per essere chiari, sto cercando metodi per convertire un oggetto in un array di byte, quindi di nuovo in un oggetto. Posso gestire la trasmissione tramite socket.


4
Dai un'occhiata a google :: protobuf , è una libreria molto potente e veloce per la serializzazione binaria. Lo abbiamo usato con successo con boost :: asio ecc.
Ketan

1
Dai un'occhiata a [STLPLUS] [1], lib con implementazione della persistenza. [1]: stlplus.sourceforge.net
lsalamon

4
Le risposte fornite non spiegano effettivamente come serializzare. Uno offre la libreria di serializzazione boost, l'altro spiega i trucchi in un'implementazione ingenua. Dato che questa è una domanda faq su c ++ , qualcuno può effettivamente rispondere?
anonimo

Risposte:


56

Parlando di serializzazione, mi viene in mente l' API di serializzazione boost . Per quanto riguarda la trasmissione dei dati serializzati sulla rete, userei i socket Berkeley o la libreria asio .

Modifica:
se desideri serializzare i tuoi oggetti in un array di byte, puoi utilizzare il serializzatore boost nel modo seguente (tratto dal sito del tutorial):

#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)
    {}
};

La serializzazione effettiva è quindi piuttosto semplice:

#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
    }

La deserializzazione funziona in modo analogo.

Esistono anche meccanismi che consentono di gestire la serializzazione dei puntatori (strutture di dati complesse come tress ecc. Non sono un problema), classi derivate e si può scegliere tra serializzazione binaria e testuale. Inoltre, tutti i contenitori STL sono supportati immediatamente.


Questa è una domanda in c ++, come mai la classe gps_position sta sovraccaricando l'operatore <<. Non è stata definita alcuna funzione amico
Vicente Bolea

notate il "boost di classe amico :: serializzazione :: accesso". Ciò fornisce l'accesso alle funzioni della libreria di serializzazione ai membri della classe anche se sono privati.
Robert Ramey

13

In alcuni casi, quando hai a che fare con tipi semplici, puoi fare:

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

Va bene come prova di concetto o prima bozza, così gli altri membri del tuo team possono continuare a lavorare su altre parti.

Ma prima o poi, di solito prima , questo ti farà male!

Hai problemi con:

  • Le tabelle dei puntatori virtuali verranno danneggiate.
  • I puntatori (a dati / membri / funzioni) verranno danneggiati.
  • Differenze di imbottitura / allineamento su macchine diverse.
  • Problemi di ordinamento dei byte Big / Little-Endian.
  • Variazioni nell'implementazione di float / double.

(Inoltre devi sapere in cosa stai disimballando sul lato ricevente.)

Puoi migliorare questo sviluppando i tuoi metodi di marshalling / unmarshalling per ogni classe. (Idealmente virtuali, quindi possono essere estesi in sottoclassi.) Alcune semplici macro ti permetteranno di scrivere diversi tipi di base abbastanza rapidamente in un ordine big / little-endian-neutro.

Ma quel tipo di lavoro grugnito è molto meglio e più facilmente gestibile tramite la libreria di serializzazione di boost .


Era qualcosa a cui stavo pensando. Ma poiché voglio serializzare su un flusso di rete, questo non funziona affatto. Al massimo per l'endianità e le diverse piattaforme. Ma non sapevo che corrompe i puntatori virtuali. Grazie =)
Atmocreations

5

C'è un modello generico che puoi usare per serializzare gli oggetti. La primitiva fondamentale sono queste due funzioni che puoi leggere e scrivere dagli iteratori:

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;
}

Quindi le funzioni di serializzazione e deserializzazione seguono lo schema:

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.
}

Per le classi è possibile utilizzare il modello di funzione amico per consentire di trovare il sovraccarico utilizzando ADL:

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.
};

Quindi nel tuo programma puoi serializzare e creare oggetti in un file come questo:

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

Quindi leggi:

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

La mia vecchia risposta qui:

Serializzare significa trasformare il tuo oggetto in dati binari. Mentre deserializzazione significa ricreare un oggetto dai dati.

Quando si serializza, si inseriscono byte in un uint8_tvettore. Quando si annulla la serializzazione, si leggono byte da un uint8_tvettore.

Ci sono certamente modelli che puoi utilizzare quando serializzi le cose.

Ogni classe serializzabile dovrebbe avere una serialize(std::vector<uint8_t> &binaryData)funzione con segno o simile che scriverà la sua rappresentazione binaria nel vettore fornito. Quindi questa funzione può passare questo vettore alle funzioni di serializzazione del membro in modo che anche loro possano scrivere le proprie cose.

Poiché la rappresentazione dei dati può essere diversa su diverse architetture. Hai bisogno di trovare uno schema su come rappresentare i dati.

Partiamo dalle basi:

Serializzazione di dati interi

Basta scrivere i byte in ordine little endian. Oppure usa la rappresentazione varint se le dimensioni contano.

Serializzazione in ordine little endian:

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

Deserializzazione dall'ordine little endian:

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

Serializzazione dei dati in virgola mobile

Per quanto ne so, l'IEEE 754 ha il monopolio qui. Non conosco nessuna architettura tradizionale che userebbe qualcos'altro per i float. L'unica cosa che può essere diversa è l'ordine dei byte. Alcune architetture usano little endian, altre usano l'ordine dei byte big endian. Ciò significa che devi stare attento a quale ordine aumentare i byte all'estremità ricevente. Un'altra differenza può essere la gestione dei valori denormale, infinito e NAN. Ma finché eviti questi valori dovresti essere OK.

Serializzazione:

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

La deserializzazione lo fa all'indietro. Attenzione all'ordine dei byte della tua architettura!

Serializzazione di stringhe

Per prima cosa devi concordare una codifica. UTF-8 è comune. Quindi memorizzalo come una lunghezza prefissata: prima memorizzi la lunghezza della stringa usando un metodo che ho menzionato sopra, quindi scrivi la stringa byte per byte.

Serializzazione di array.

Sono gli stessi degli archi. Si serializza prima un numero intero che rappresenta la dimensione dell'array, quindi si serializza ogni oggetto in esso.

Serializzazione di interi oggetti

Come ho detto prima, dovrebbero avere un serializemetodo che aggiunga contenuto a un vettore. Per deserializzare un oggetto, dovrebbe avere un costruttore che accetta il flusso di byte. Può essere un istreamma nel caso più semplice può essere solo un uint8_tpuntatore di riferimento . Il costruttore legge i byte che desidera dal flusso e imposta i campi nell'oggetto. Se il sistema è ben progettato e serializza i campi nell'ordine dei campi dell'oggetto, puoi semplicemente passare il flusso ai costruttori del campo in un elenco di inizializzatori e deserializzarli nell'ordine corretto.

Serializzazione di grafici a oggetti

Per prima cosa devi assicurarti che questi oggetti siano davvero qualcosa che vuoi serializzare. Non è necessario serializzarli se le istanze di questi oggetti sono presenti nella destinazione.

Ora hai scoperto che devi serializzare quell'oggetto puntato da un puntatore. Il problema dei puntatori che sono validi solo nel programma che li utilizza. Non puoi serializzare il puntatore, dovresti smettere di usarli negli oggetti. Crea invece pool di oggetti. Questo pool di oggetti è fondamentalmente un array dinamico che contiene "scatole". Queste caselle hanno un conteggio dei riferimenti. Il conteggio dei riferimenti diverso da zero indica un oggetto attivo, lo zero indica uno slot vuoto. Quindi crei un puntatore intelligente simile a shared_ptr che non memorizza il puntatore all'oggetto, ma l'indice nell'array. È inoltre necessario concordare un indice che denoti il ​​puntatore nullo, ad es. -1.

Fondamentalmente quello che abbiamo fatto qui è stato sostituito i puntatori con indici di matrice. Ora durante la serializzazione puoi serializzare questo indice di array come al solito. Non è necessario preoccuparsi di dove sarà l'oggetto in memoria nel sistema di destinazione. Assicurati solo che abbiano lo stesso pool di oggetti.

Quindi dobbiamo serializzare i pool di oggetti. Ma quali? Bene, quando serializzi un oggetto grafico non stai serializzando solo un oggetto, stai serializzando un intero sistema. Ciò significa che la serializzazione del sistema non dovrebbe iniziare da parti del sistema. Questi oggetti non dovrebbero preoccuparsi del resto del sistema, hanno solo bisogno di serializzare gli indici dell'array e il gioco è fatto. È necessario disporre di una routine del serializzatore di sistema che orchestra la serializzazione del sistema e passa attraverso i pool di oggetti pertinenti e li serializza tutti.

All'estremità ricevente tutti gli array e gli oggetti all'interno vengono deserializzati, ricreando l'oggetto grafico desiderato.

Serializzazione dei puntatori a funzione

Non memorizzare i puntatori nell'oggetto. Avere un array statico che contiene i puntatori a queste funzioni e memorizzare l'indice nell'oggetto.

Poiché entrambi i programmi hanno questa tabella compilata negli scaffali, dovrebbe funzionare solo l'indice.

Serializzazione di tipi polimorfici

Poiché ho detto che dovresti evitare i puntatori nei tipi serializzabili e dovresti invece usare gli indici degli array, il polimorfismo non può funzionare, perché richiede puntatori.

È necessario aggirare questo problema con tag di tipo e unioni.

Controllo delle versioni

In cima a tutto quanto sopra. Potresti volere che diverse versioni del software interagiscano.

In questo caso ogni oggetto dovrebbe scrivere un numero di versione all'inizio della loro serializzazione per indicare la versione.

Quando si carica l'oggetto dall'altra parte, gli oggetti più recenti potrebbero essere in grado di gestire le rappresentazioni più vecchie, ma quelli più vecchi non possono gestire le più recenti, quindi dovrebbero generare un'eccezione a riguardo.

Ogni volta che qualcosa cambia, dovresti battere il numero di versione.


Quindi, per concludere, la serializzazione può essere complessa. Ma fortunatamente non è necessario serializzare tutto nel programma, molto spesso vengono serializzati solo i messaggi del protocollo, che sono spesso semplici strutture vecchie. Quindi non hai bisogno dei trucchi complessi che ho menzionato sopra troppo spesso.


1
Grazie. Questa risposta contiene un'ottima panoramica dei concetti relativi alla serializzazione dei dati strutturati in C ++.
Sean

0

Per imparare ho scritto un semplice serializzatore C ++ 11. Avevo provato varie delle altre offerte più pesanti, ma volevo qualcosa che potessi effettivamente capire quando è andato storto o non è riuscito a compilare con l'ultimo g ++ (cosa che è successo per me con Cereal; una libreria davvero bella ma complessa e non riuscivo a grok gli errori che il compilatore ha vomitato durante l'aggiornamento.) Ad ogni modo, è solo intestazione e gestisce i tipi di POD, contenitori, mappe ecc ... Nessun controllo delle versioni e caricherà solo i file dalla stessa architettura in cui è stato salvato.

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

Utilizzo di esempio:

#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);
}

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.