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)
{
}
template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
}
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;
template <class OutputCharIterator>
friend void serialize(const Foo &obj, OutputCharIterator &&it)
{
}
};
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_t
vector. Al anular la serialización, está leyendo bytes de un uint8_t
vector.
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 serialize
mé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 istream
pero, en el caso más simple, puede ser solo un uint8_t
puntero 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.