¿Cómo serializas un objeto en C ++?

84

Tengo una pequeña jerarquía de objetos que necesito serializar y transmitir a través de una conexión de socket. Necesito serializar el objeto y luego deserializarlo según el tipo que sea. ¿Existe una manera fácil de hacer esto en C ++ (como la hay en Java)?

¿Hay ejemplos o tutoriales de código en línea de serialización de C ++?

EDITAR: Para que quede claro, estoy buscando métodos para convertir un objeto en una matriz de bytes y luego volver a convertirlo en un objeto. Puedo manejar la transmisión de socket.

Bill el lagarto
fuente
3
Consulte google :: protobuf , es una biblioteca muy fuerte y rápida para la serialización binaria. Lo hemos usado con éxito con boost :: asio, etc.
Ketan
Eche un vistazo a [STLPLUS] [1], lib con implementación de persistencia. [1]: stlplus.sourceforge.net
lsalamon
4
Las respuestas proporcionadas en realidad no explican cómo serializar. Uno ofrece la biblioteca de serialización boost, el otro explica errores en una implementación ingenua. Dado que esta es c ++ - faq question, ¿alguien realmente puede responderla?
anónimo

Respuestas:

55

Hablando de serialización, me viene a la mente la API de serialización boost . En cuanto a transmitir los datos serializados a través de la red, usaría sockets de Berkeley o la biblioteca asio .

Editar:
si desea serializar sus objetos en una matriz de bytes, puede usar el serializador boost de la siguiente manera (tomado del sitio 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 serialización real es entonces bastante fácil:

#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 deserialización funciona de manera análoga.

También hay mecanismos que le permiten manejar la serialización de punteros (estructuras de datos complejas como árboles, etc. no son un problema), clases derivadas y puede elegir entre serialización binaria y de texto. Además, todos los contenedores STL son compatibles desde el primer momento.

Newgre
fuente
Esta es una pregunta de C ++, ¿cómo es que la clase gps_position está sobrecargando el operador <<. No hay ninguna función de amigo definida
Vicente Bolea
observe el "aumento de clase amigo :: serialización :: acceso". Esto proporciona acceso a las funciones de la biblioteca de serialización a los miembros de la clase, incluso si son privados.
Robert Ramey
13

En algunos casos, cuando se trata de tipos simples, puede hacer:

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

Está bien como prueba de concepto o como primer borrador, para que otros miembros de su equipo puedan seguir trabajando en otras partes.

Pero tarde o temprano, generalmente antes , ¡esto te hará daño!

Tiene problemas con:

  • Las tablas de punteros virtuales se dañarán.
  • Los punteros (a datos / miembros / funciones) se dañarán.
  • Diferencias en el acolchado / alineación en diferentes máquinas.
  • Problemas de ordenación de bytes Big / Little-Endian.
  • Variaciones en la implementación de float / double.

(Además, necesita saber en qué está desempacando en el lado de recepción).

Puede mejorar esto desarrollando sus propios métodos de ordenación / desglose para cada clase. (Idealmente virtuales, para que puedan extenderse en subclases). Unas pocas macros simples le permitirán escribir diferentes tipos básicos con bastante rapidez en un orden neutral de endian grande / pequeño.

Pero ese tipo de trabajo pesado es mucho mejor y más fácil de manejar a través de la biblioteca de serialización de boost .

Señor ree
fuente
Eso era algo en lo que estaba pensando. Pero como quiero serializar en un flujo de red, esto no funciona en absoluto. A lo sumo por la endianidad y las diferentes plataformas. Pero no sabía que corrompe los punteros virtuales. Gracias =)
Atmocreations
4

Existe un patrón genérico que puede utilizar para serializar objetos. La primitiva fundamental son estas dos funciones que puede leer y escribir desde iteradores:

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

Luego, las funciones de serialización y deserialización siguen el patrón:

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

Para las clases, puede usar el patrón de función de amigo para permitir que la sobrecarga se encuentre usando 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.
};

En su programa, puede serializar y objetar en un archivo como este:

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

Entonces lee:

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

Mi vieja respuesta aquí:

La serialización significa convertir su objeto en datos binarios. Mientras que la deserialización significa recrear un objeto a partir de los datos.

Al serializar, está insertando bytes en un uint8_tvector. Al anular la serialización, está leyendo bytes de un uint8_tvector.

Ciertamente, hay patrones que puede emplear al serializar cosas.

Cada clase serializable debe tener una serialize(std::vector<uint8_t> &binaryData)función firmada o similar que escribirá su representación binaria en el vector proporcionado. Entonces, esta función puede pasar este vector a las funciones de serialización de sus miembros para que también puedan escribir sus cosas en él.

Dado que la representación de datos puede ser diferente en diferentes arquitecturas. Necesita encontrar un esquema de cómo representar los datos.

Empecemos por lo básico:

Serializar datos enteros

Simplemente escriba los bytes en orden little endian. O use la representación de varint si el tamaño importa.

Serialización en orden 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);

Deserialización del orden little endian:

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

Serializar datos de coma flotante

Hasta donde yo sé, el IEEE 754 tiene un monopolio aquí. No conozco ninguna arquitectura convencional que use algo más para flotadores. Lo único que puede ser diferente es el orden de los bytes. Algunas arquitecturas usan little endian, otras usan un orden de bytes big endian. Esto significa que debe tener cuidado con el orden en el que se emiten los bytes en el extremo receptor. Otra diferencia puede ser el manejo de los valores denormal e infinito y NAN. Pero siempre que evite estos valores, debería estar bien.

Publicación por entregas:

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

La deserialización lo está haciendo al revés. ¡Cuidado con el orden de bytes de tu arquitectura!

Serializar cadenas

Primero debes acordar una codificación. UTF-8 es común. Luego, guárdelo como una longitud prefijada: primero almacena la longitud de la cadena usando un método que mencioné anteriormente, luego escribe la cadena byte por byte.

Serializar matrices.

Son lo mismo que las cuerdas. Primero serializa un número entero que representa el tamaño de la matriz y luego serializa cada objeto en él.

Serializar objetos completos

Como dije antes, deberían tener un serializemétodo que agregue contenido a un vector. Para anular la serialización de un objeto, debe tener un constructor que tome un flujo de bytes. Puede ser un istreampero, en el caso más simple, puede ser solo un uint8_tpuntero de referencia . El constructor lee los bytes que quiere de la secuencia y configura los campos en el objeto. Si el sistema está bien diseñado y serializa los campos en el orden de los campos del objeto, simplemente puede pasar la secuencia a los constructores del campo en una lista de inicializadores y deserializarlos en el orden correcto.

Serializar gráficos de objetos

Primero debe asegurarse de que estos objetos sean realmente algo que desee serializar. No necesita serializarlos si hay instancias de estos objetos presentes en el destino.

Ahora descubrió que necesita serializar ese objeto apuntado por un puntero. El problema de los punteros que son válidos solo en el programa que los usa. No puede serializar punteros, debe dejar de usarlos en objetos. En su lugar, cree grupos de objetos. Este grupo de objetos es básicamente una matriz dinámica que contiene "cajas". Estos cuadros tienen un recuento de referencias. El recuento de referencia distinto de cero indica un objeto vivo, cero indica un espacio vacío. Luego, crea un puntero inteligente similar al shared_ptr que no almacena el puntero al objeto, sino el índice en la matriz. También debe acordar un índice que denote el puntero nulo, por ejemplo. -1.

Básicamente, lo que hicimos aquí fue reemplazar los punteros con índices de matriz. Ahora, al serializar, puede serializar este índice de matriz como de costumbre. No necesita preocuparse de dónde estará el objeto en la memoria en el sistema de destino. Solo asegúrate de que también tengan el mismo grupo de objetos.

Entonces necesitamos serializar los grupos de objetos. Pero cuales? Bueno, cuando serializa un gráfico de objeto, no está serializando solo un objeto, está serializando un sistema completo. Esto significa que la serialización del sistema no debe comenzar desde partes del sistema. Esos objetos no deberían preocuparse por el resto del sistema, solo necesitan serializar los índices de la matriz y eso es todo. Debe tener una rutina de serializador del sistema que orquesta la serialización del sistema y recorra los grupos de objetos relevantes y serialice todos.

En el extremo receptor, todas las matrices y los objetos dentro se deserializan, recreando el gráfico de objeto deseado.

Punteros de función de serialización

No almacene punteros en el objeto. Tenga una matriz estática que contenga los punteros a estas funciones y almacene el índice en el objeto.

Dado que ambos programas tienen esta tabla compilada en sus estantes, usar solo el índice debería funcionar.

Serializar tipos polimórficos

Como dije que debería evitar los punteros en tipos serializables y que debería usar índices de matriz en su lugar, el polimorfismo simplemente no puede funcionar porque requiere punteros.

Necesita solucionar esto con etiquetas de tipo y uniones.

Control de versiones

Además de todo lo anterior. Es posible que desee que interoperen diferentes versiones del software.

En este caso, cada objeto debe escribir un número de versión al comienzo de su serialización para indicar la versión.

Al cargar el objeto en el otro lado, los objetos más nuevos pueden manejar las representaciones más antiguas, pero los más antiguos no pueden manejar las más nuevas, por lo que deberían lanzar una excepción al respecto.

Cada vez que algo cambia, debes aumentar el número de versión.


Entonces, para terminar, la serialización puede ser compleja. Pero, afortunadamente, no necesita serializar todo en su programa, la mayoría de las veces solo se serializan los mensajes del protocolo, que a menudo son estructuras viejas. Así que no necesitas los complejos trucos que mencioné anteriormente con demasiada frecuencia.

Calmarius
fuente
1
Gracias. Esta respuesta contiene una gran descripción general de los conceptos relevantes para la serialización de datos estructurados en C ++.
Sean
0

A modo de aprendizaje, escribí un simple serializador de C ++ 11. Había probado varias de las otras ofertas más pesadas, pero quería algo que realmente pudiera entender cuando saliera mal o no se compilara con la última versión de g ++ (lo que me sucedió con Cereal; una biblioteca realmente agradable pero compleja y no pude asimilar los errores que el compilador arrojó en la actualización). De todos modos, es solo el encabezado y maneja tipos de POD, contenedores, mapas, etc. Sin control de versiones y solo cargará archivos del mismo arco en el que se guardó.

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

Uso de ejemplo:

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