Существует общий шаблон, который можно использовать для сериализации объектов. Фундаментальный примитив - это две функции, которые вы можете читать и писать с помощью итераторов:
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;
}
Затем функции сериализации и десериализации следуют шаблону:
template <class OutputCharIterator>
void serialize(const YourType &obj, OutputCharIterator &&it)
{
}
template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
}
Для классов вы можете использовать шаблон функции друга, чтобы разрешить обнаружение перегрузки с помощью ADL:
class Foo
{
int internal1, internal2;
template <class OutputCharIterator>
friend void serialize(const Foo &obj, OutputCharIterator &&it)
{
}
};
Затем в своей программе вы можете сериализовать и объектно преобразовать в файл следующим образом:
std::ofstream file("savestate.bin");
serialize(yourObject, std::ostreambuf_iterator<char>(file));
Тогда прочтите:
std::ifstream file("savestate.bin");
deserialize(yourObject, std::istreamBuf_iterator<char>(file), std::istreamBuf_iterator<char>());
Мой старый ответ здесь:
Сериализация означает превращение вашего объекта в двоичные данные. В то время как десериализация означает воссоздание объекта из данных.
При сериализации вы помещаете байты в uint8_t
вектор. При десериализации вы читаете байты изuint8_t
вектора.
Конечно, есть шаблоны, которые вы можете использовать при сериализации.
Каждый сериализуемый класс должен иметь serialize(std::vector<uint8_t> &binaryData)
или аналогичную функцию с подписью, которая будет записывать свое двоичное представление в предоставленный вектор. Затем эта функция может передать этот вектор функциям сериализации своего члена, чтобы они тоже могли записывать в него свои данные.
Поскольку представление данных может отличаться на разных архитектурах. Вам нужно выяснить схему, как представить данные.
Начнем с основ:
Сериализация целочисленных данных
Просто запишите байты в обратном порядке. Или используйте представление varint, если размер имеет значение.
Сериализация в порядке прямого байта:
data.push_back(integer32 & 0xFF);
data.push_back((integer32 >> 8) & 0xFF);
data.push_back((integer32 >> 16) & 0xFF);
data.push_back((integer32 >> 24) & 0xFF);
Десериализация с прямым порядком байтов:
integer32 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
Сериализация данных с плавающей запятой
Насколько я знаю, IEEE 754 имеет здесь монополию. Я не знаю ни одной основной архитектуры, которая использовала бы что-то еще для поплавков. Единственное, что может отличаться - это порядок байтов. Некоторые архитектуры используют прямой порядок байтов, другие - большой порядок байтов. Это означает, что вам нужно быть осторожным в том, в каком порядке вы увеличиваете байты на принимающей стороне. Еще одно отличие может заключаться в обработке значений денормали и бесконечности и NAN. Но пока вы избегаете этих значений, все будет в порядке.
Сериализация:
uint8_t mem[8];
memcpy(mem, doubleValue, 8);
data.push_back(mem[0]);
data.push_back(mem[1]);
...
Десериализация делает это наоборот. Обратите внимание на порядок байтов в вашей архитектуре!
Сериализация строк
Для начала нужно согласовать кодировку. UTF-8 является распространенным. Затем сохраните его в виде префикса длины: сначала вы сохраняете длину строки, используя метод, который я упомянул выше, затем записываете строку побайтно.
Сериализация массивов.
Они такие же, как струны. Сначала вы сериализуете целое число, представляющее размер массива, а затем сериализуете каждый объект в нем.
Сериализация целых объектов
Как я уже сказал, у них должен быть serialize
метод, добавляющий контент в вектор. Чтобы десериализовать объект, у него должен быть конструктор, принимающий байтовый поток. Это может быть, istream
но в простейшем случае это может быть просто ссылкаuint8_t
указатель . Конструктор считывает нужные байты из потока и настраивает поля в объекте. Если система хорошо спроектирована и сериализует поля в порядке полей объекта, вы можете просто передать поток конструкторам поля в списке инициализаторов и десериализовать их в правильном порядке.
Сериализация графов объектов
Сначала вам нужно убедиться, действительно ли эти объекты нужно сериализовать. Вам не нужно сериализовать их, если экземпляры этих объектов присутствуют в месте назначения.
Теперь вы узнали, что вам нужно сериализовать этот объект, на который указывает указатель. Проблема указателей в том, что они действительны только в той программе, которая их использует. Вы не можете сериализовать указатель, вы должны прекратить использовать их в объектах. Вместо этого создавайте пулы объектов. Этот пул объектов в основном представляет собой динамический массив, содержащий «ящики». Эти коробки имеют счетчик ссылок. Ненулевой счетчик ссылок указывает на активный объект, ноль указывает на пустой слот. Затем вы создаете интеллектуальный указатель, похожий на shared_ptr, который хранит не указатель на объект, а индекс в массиве. Вам также необходимо согласовать индекс, который обозначает нулевой указатель, например. -1.
В основном то, что мы здесь сделали, - это замена указателей на индексы массива. Теперь при сериализации вы можете сериализовать этот индекс массива как обычно. Вам не нужно беспокоиться о том, где будет находиться объект в памяти целевой системы. Просто убедитесь, что у них тоже один и тот же пул объектов.
Итак, нам нужно сериализовать пулы объектов. Но какие? Что ж, когда вы сериализуете граф объектов, вы сериализуете не только объект, вы сериализуете всю систему. Это означает, что сериализация системы не должна начинаться с отдельных частей системы. Эти объекты не должны беспокоиться об остальной части системы, им нужно только сериализовать индексы массива и все. У вас должна быть процедура системного сериализатора, которая организует сериализацию системы и проходит через соответствующие пулы объектов и сериализует их все.
На принимающей стороне все массивы и объекты внутри десериализуются, воссоздавая желаемый граф объектов.
Сериализация указателей на функции
Не храните указатели в объекте. Имейте статический массив, который содержит указатели на эти функции и хранит индекс в объекте.
Поскольку обе программы скомпилировали эту таблицу на своих полках, использование только индекса должно работать.
Сериализация полиморфных типов
Поскольку я сказал, что вам следует избегать указателей в сериализуемых типах и вместо этого использовать индексы массивов, полиморфизм просто не может работать, потому что он требует указателей.
Вам нужно обойти это с помощью тегов типов и объединений.
Управление версиями
Вдобавок ко всему вышесказанному. Возможно, вы захотите, чтобы разные версии программного обеспечения взаимодействовали друг с другом.
В этом случае каждый объект должен написать номер версии в начале своей сериализации, чтобы указать версию.
При загрузке объекта на другой стороне более новые объекты могут обрабатывать более старые представления, но более старые не могут обрабатывать более новые, поэтому они должны генерировать исключение по этому поводу.
Каждый раз, когда что-то меняется, вы должны увеличивать номер версии.
Итак, сериализация может быть сложной задачей. Но, к счастью, вам не нужно сериализовать все в вашей программе, чаще всего сериализуются только сообщения протокола, которые часто являются простыми старыми структурами. Так что вам не нужны слишком часто сложные приемы, о которых я упоминал выше.