Сериализация данных в C++ с библиотекой Cereal

—

от автора

Привет, Хабр!

Сегодня рассмотрим библиотеку Cereal в C++, которая позволяет сохранять и загружать состояние объектов, не теряя производительности.

Cereal — это заголовочная библиотека для C++, предназначенная для сериализации данных. Она поддерживает XML и JSON. Помимо этого поддерживает практически все стандартные типы данных в C++ и имеет инструменты для работы с пользовательскими типами. В отличие от, например, библиотек Boost, Cereal не требует сложных настроек и имеет интуитивно понятный синтаксис, знакомый юзерам Boost.

Установим

Cкачаем последнюю версию библиотеки с GitHub:

git clone https://github.com/USCiLab/cereal.git

После скачивания переходим в папку include/cereal в корневом каталоге проекта. Копируем эту папку в директорию, доступную для проекта.

Cereal является заголовочной библиотекой, поэтому дополнительная компиляция не требуется!

Cereal требует компилятора, поддерживающего стандарт C++11. Список поддерживаемых компиляторов:

  • GCC 4.7.3 или новее

  • Clang 3.3 или новее

  • MSVC 2013 или новее

Основной синтаксис

Функции serialize

Функция serialize — основной метод для определения, какие члены класса должны быть сериализованы. Обычно её определяют внутри класса:

struct MyRecord {     uint8_t x, y;     float z;      template <class Archive>     void serialize(Archive& ar) {         ar(x, y, z);     } };

Здесь функция serialize принимает объект архива и передает ему члены класса для сериализации.

Функции save и load

Когда нужно разделить процесс сериализации на загрузку и сохранение, можно юзать функции save и load. Мастхев, когда требуется выполнять доп. действия при загрузке или сохранении данных:

struct SomeData {     int32_t id;     std::shared_ptr<std::unordered_map<uint32_t, MyRecord>> data;      template <class Archive>     void save(Archive& ar) const {         ar(data);     }      template <class Archive>     void load(Archive& ar) {         static int32_t idGen = 0;         id = idGen++;         ar(data);     } };

Функция save должна быть const, т.к она не должна изменять состояние объекта.

Функции save_minimal и load_minimal

Эти функции позволяют минимизировать выходные данные, представляя объект в виде одного примитива или строки. Полезно для упрощения человекочитаемых архивов:

struct MyData {     double d;      template <class Archive>     double save_minimal(Archive const&) const {         return d;     }      template <class Archive>     void load_minimal(Archive const&, double const& value) {         d = value;     } };

Умные указатели

Cereal поддерживает сериализацию умных указателей std::shared_ptr и std::unique_ptr. Так можно ериализовать объекты, на которые ссылаются умные указатели, без лшних усилий:

#include <cereal/types/memory.hpp>  struct DataHolder {     std::shared_ptr<MyRecord> record;      template <class Archive>     void serialize(Archive& ar) {         ar(record);     } };

Наследование

Есть функции cereal::base_class и cereal::virtual_base_class, которые помогают корректно сериализовать базовые и производные классы:

#include <cereal/types/base_class.hpp>  struct Base {     int x;      template <class Archive>     void serialize(Archive& ar) {         ar(x);     } };  struct Derived : public Base {     int y;      template <class Archive>     void serialize(Archive& ar) {         ar(cereal::base_class<Base>(this), y);     } };

Архивы и их типы

Cereal поддерживает несколько типов архивов: бинарные, XML и JSON архивы. Каждый из них используется для сериализации данных в различных форматах.

Бинарные архивы

#include <cereal/archives/binary.hpp>  std::ofstream os("data.cereal", std::ios::binary); cereal::BinaryOutputArchive archive(os); archive(someData);

XML архивы

#include <cereal/archives/xml.hpp>  std::ofstream os("data.xml"); cereal::XMLOutputArchive archive(os); archive(someData);

JSON архивы

#include <cereal/archives/json.hpp>  std::ofstream os("data.json"); cereal::JSONOutputArchive archive(os); archive(someData);

Версионирование типов

Для управления версиями типов есть макрос CEREAL_CLASS_VERSION, который позволяет задавать версию для каждого типа данных:

#include <cereal/types/base_class.hpp> #include <cereal/types/polymorphic.hpp>  struct MyType {     int x;      template <class Archive>     void serialize(Archive& ar, const std::uint32_t version) {         ar(x);     } };  CEREAL_CLASS_VERSION(MyType, 1);

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

Сохранение и загрузка конфигурации приложения

Часто Cereal юзают для сериализация конфигурационных файлов. Рассмотрим пример, где конфигурация приложения хранится в JSON файле, и хотелось бы её сохранять и загружать при запуске приложения:

#include <cereal/archives/json.hpp> #include <cereal/types/map.hpp> #include <cereal/types/string.hpp> #include <fstream> #include <iostream>  struct AppConfig {     std::map<std::string, std::string> settings;      template <class Archive>     void serialize(Archive& ar) {         ar(settings);     } };  void saveConfig(const AppConfig& config, const std::string& filename) {     std::ofstream os(filename);     cereal::JSONOutputArchive archive(os);     archive(config); }  AppConfig loadConfig(const std::string& filename) {     std::ifstream is(filename);     cereal::JSONInputArchive archive(is);     AppConfig config;     archive(config);     return config; }  int main() {     AppConfig config;     config.settings["username"] = "admin";     config.settings["theme"] = "dark";      saveConfig(config, "config.json");      AppConfig loadedConfig = loadConfig("config.json");     std::cout << "Username: " << loadedConfig.settings["username"] << "\n";     std::cout << "Theme: " << loadedConfig.settings["theme"] << "\n";      return 0; }

Сохранение состояния игры

В игрушках было бы хорошо сохранять состояние игры, чтобы игрок мог продолжить с того места, где остановился. Можно легко сохранять и загружать данные игры с помощью Cereal:

#include <cereal/archives/binary.hpp> #include <cereal/types/vector.hpp> #include <fstream>  struct GameState {     int level;     int score;     std::vector<int> inventory;      template <class Archive>     void serialize(Archive& ar) {         ar(level, score, inventory);     } };  void saveGameState(const GameState& state, const std::string& filename) {     std::ofstream os(filename, std::ios::binary);     cereal::BinaryOutputArchive archive(os);     archive(state); }  GameState loadGameState(const std::string& filename) {     std::ifstream is(filename, std::ios::binary);     cereal::BinaryInputArchive archive(is);     GameState state;     archive(state);     return state; }  int main() {     GameState state{3, 4500, {1, 2, 3}};     saveGameState(state, "game.sav");      GameState loadedState = loadGameState("game.sav");     std::cout << "Level: " << loadedState.level << "\n";     std::cout << "Score: " << loadedState.score << "\n";     std::cout << "Inventory: ";     for (int item : loadedState.inventory) {         std::cout << item << " ";     }     std::cout << "\n";      return 0; }

Сериализация данных для сетевого обмена

В распределенных системах и сетевых приложениях в основном нужно сериализовать данные для передачи по сети.

Пример:

#include <cereal/archives/binary.hpp> #include <cereal/types/string.hpp> #include <cereal/types/vector.hpp> #include <sstream> #include <iostream>  struct Message {     std::string sender;     std::string content;     std::vector<int> attachments;      template <class Archive>     void serialize(Archive& ar) {         ar(sender, content, attachments);     } };  std::string serializeMessage(const Message& message) {     std::ostringstream oss;     cereal::BinaryOutputArchive archive(oss);     archive(message);     return oss.str(); }  Message deserializeMessage(const std::string& data) {     std::istringstream iss(data);     cereal::BinaryInputArchive archive(iss);     Message message;     archive(message);     return message; }  int main() {     Message msg = {"Alice", "Hello, Bob!", {1, 2, 3}};     std::string serializedData = serializeMessage(msg);      Message deserializedMsg = deserializeMessage(serializedData);     std::cout << "Sender: " << deserializedMsg.sender << ", Content: " << deserializedMsg.content << std::endl;      return 0; }

Как разработчику на С++ организовать кроссплатформенную разработку? Об этом расскажет Арсений Черенков. Встречаемся на бесплатном практическом уроке «Менеджер пакетов Conan для С++проектов» от OTUS.


ссылка на оригинал статьи https://habr.com/ru/articles/821195/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *