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)
{
}
template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
}
Per le classi è possibile utilizzare il modello di funzione amico per consentire di trovare il sovraccarico utilizzando ADL:
class Foo
{
int internal1, internal2;
template <class OutputCharIterator>
friend void serialize(const Foo &obj, OutputCharIterator &&it)
{
}
};
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_t
vettore. Quando si annulla la serializzazione, si leggono byte da un uint8_t
vettore.
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 serialize
metodo che aggiunga contenuto a un vettore. Per deserializzare un oggetto, dovrebbe avere un costruttore che accetta il flusso di byte. Può essere un istream
ma nel caso più semplice può essere solo un uint8_t
puntatore 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.