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

Сегодня рассмотрим библиотеку 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.

Комментарии (2)


  1. sena
    23.06.2024 19:48

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


  1. sha512sum
    23.06.2024 19:48

    Хочется иметь то, что работает на основе того же boost pfr, чтобы не писать бойлерплейт код. Ну и гибкую настройку этого всего.