Как вы сериализуете объект в C ++?

87

У меня есть небольшая иерархия объектов, которые мне нужно сериализовать и передать через соединение сокета. Мне нужно сериализовать объект, а затем десериализовать его в зависимости от типа. Есть ли простой способ сделать это в C ++ (как в Java)?

Существуют ли онлайн-образцы кода или учебные пособия по сериализации C ++?

РЕДАКТИРОВАТЬ: Чтобы быть ясным, я ищу методы преобразования объекта в массив байтов, а затем обратно в объект. Я справлюсь с розеткой передачи.

Билл Ящерица
источник
4
Обратите внимание на google :: protobuf , это очень сильная и быстрая библиотека для двоичной сериализации. Мы успешно использовали его с boost :: asio и т. Д.
Кетан
1
Взгляните на [STLPLUS] [1], библиотека с реализацией постоянства. [1]: stlplus.sourceforge.net
lsalamon
4
Предоставленные ответы на самом деле не объясняют, как сериализовать. Один предлагает библиотеку ускоренной сериализации, другой объясняет подводные камни наивной реализации. Так как это вопрос часто задаваемых вопросов по C ++ , может ли кто-нибудь на него ответить?
анонимно,

Ответы:

56

Говоря о сериализации, мне на ум приходит API ускоренной сериализации . Что касается передачи сериализованных данных по сети, я бы использовал сокеты Беркли или библиотеку asio .

Изменить:
если вы хотите сериализовать свои объекты в байтовый массив, вы можете использовать сериализатор повышения следующим образом (взято с учебного сайта):

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

Фактическая сериализация тогда довольно проста:

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

Аналогичным образом работает десериализация.

Существуют также механизмы, которые позволяют обрабатывать сериализацию указателей (сложные структуры данных, такие как tress и т. Д., Не являются проблемой), производные классы, и вы можете выбирать между двоичной и текстовой сериализацией. Кроме того, все контейнеры STL поддерживаются из коробки.

Newgre
источник
Это вопрос C ++, почему класс gps_position перегружает оператор <<. Функция друга не определена
Vicente Bolea
обратите внимание на "класс друга boost :: serialization :: access". Это обеспечивает доступ к функциям библиотеки сериализации для членов класса, даже если они являются частными.
Роберт Рэйми,
13

В некоторых случаях, имея дело с простыми типами, вы можете:

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

Это нормально в качестве доказательства концепции или первого черновика, поэтому другие члены вашей команды могут продолжать работать над другими частями.

Но рано или поздно, обычно рано , это причинит вам боль!

У вас возникают проблемы с:

  • Таблицы виртуальных указателей будут повреждены.
  • Указатели (на данные / элементы / функции) будут повреждены.
  • Различия в заполнении / выравнивании на разных машинах.
  • Проблемы с порядком байтов Big / Little-Endian.
  • Варианты реализации float / double.

(К тому же вам нужно знать, что вы распаковываете на принимающей стороне.)

Вы можете улучшить это, разработав свои собственные методы маршалинга / демаршалинга для каждого класса. (В идеале виртуальные, чтобы их можно было расширить в подклассы.) Несколько простых макросов позволят вам довольно быстро записывать различные базовые типы в нейтральном порядке с прямым порядком байтов / прямым порядком байтов.

Но такая ворчливая работа намного лучше и легче выполняется с помощью библиотеки сериализации boost .

Мистер Ри
источник
Я думал об этом. Но поскольку я хочу сериализовать в сетевой поток, это вообще не работает. Максимум из-за порядка байтов и разных платформ. Но я не знал, что это портит виртуальные указатели. Спасибо =)
Atmocreations
5

Существует общий шаблон, который можно использовать для сериализации объектов. Фундаментальный примитив - это две функции, которые вы можете читать и писать с помощью итераторов:

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)
{
    // Call putbyte or other serialize overloads.
}

template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
    // Call getByte or other deserialize overloads.
}

Для классов вы можете использовать шаблон функции друга, чтобы разрешить обнаружение перегрузки с помощью 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.
};

Затем в своей программе вы можете сериализовать и объектно преобразовать в файл следующим образом:

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.

В основном то, что мы здесь сделали, - это замена указателей на индексы массива. Теперь при сериализации вы можете сериализовать этот индекс массива как обычно. Вам не нужно беспокоиться о том, где будет находиться объект в памяти целевой системы. Просто убедитесь, что у них тоже один и тот же пул объектов.

Итак, нам нужно сериализовать пулы объектов. Но какие? Что ж, когда вы сериализуете граф объектов, вы сериализуете не только объект, вы сериализуете всю систему. Это означает, что сериализация системы не должна начинаться с отдельных частей системы. Эти объекты не должны беспокоиться об остальной части системы, им нужно только сериализовать индексы массива и все. У вас должна быть процедура системного сериализатора, которая организует сериализацию системы и проходит через соответствующие пулы объектов и сериализует их все.

На принимающей стороне все массивы и объекты внутри десериализуются, воссоздавая желаемый граф объектов.

Сериализация указателей на функции

Не храните указатели в объекте. Имейте статический массив, который содержит указатели на эти функции и хранит индекс в объекте.

Поскольку обе программы скомпилировали эту таблицу на своих полках, использование только индекса должно работать.

Сериализация полиморфных типов

Поскольку я сказал, что вам следует избегать указателей в сериализуемых типах и вместо этого использовать индексы массивов, полиморфизм просто не может работать, потому что он требует указателей.

Вам нужно обойти это с помощью тегов типов и объединений.

Управление версиями

Вдобавок ко всему вышесказанному. Возможно, вы захотите, чтобы разные версии программного обеспечения взаимодействовали друг с другом.

В этом случае каждый объект должен написать номер версии в начале своей сериализации, чтобы указать версию.

При загрузке объекта на другой стороне более новые объекты могут обрабатывать более старые представления, но более старые не могут обрабатывать более новые, поэтому они должны генерировать исключение по этому поводу.

Каждый раз, когда что-то меняется, вы должны увеличивать номер версии.


Итак, сериализация может быть сложной задачей. Но, к счастью, вам не нужно сериализовать все в вашей программе, чаще всего сериализуются только сообщения протокола, которые часто являются простыми старыми структурами. Так что вам не нужны слишком часто сложные приемы, о которых я упоминал выше.

Кальмарий
источник
1
Спасибо. Этот ответ содержит отличный обзор концепций, относящихся к сериализации структурированных данных в C ++.
Шон
0

В качестве обучения я написал простой сериализатор на C ++ 11. Я пробовал различные другие, более тяжелые предложения, но хотел что-то, что я действительно мог понять, когда что-то пошло не так или не удалось скомпилировать с последней версией g ++ (что случилось со мной с Cereal; действительно хорошая библиотека, но сложная, и я не мог понять Ошибки, которые компилятор выдавал при обновлении.) В любом случае, это только заголовок и обрабатывает типы POD, контейнеры, карты и т. д. Без управления версиями, и он будет загружать файлы только из той же архитектуры, в которой был сохранен.

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

Пример использования:

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

Нил МакГилл
источник