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