Serializing data in C++ with the Cereal library

BoostCereal does not require complex settings and has an intuitive syntax that is familiar to Boost users.

Let's install

Download the latest version of the library from GitHub:

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

After downloading, go to the folder include/cereal in the project root directory. Copy this folder to a directory accessible to the project.

Cereal is a header library, so no additional compilation is required!

Cereal requires a compiler that supports the C++11 standard. List of supported compilers:

  • GCC 4.7.3 or later

  • Clang 3.3 or later

  • MSVC 2013 or later

Basic syntax

serialize functions

Function serialize – the main method for determining which class members should be serialized. Usually it is defined inside a class:

struct MyRecord {
    uint8_t x, y;
    float z;

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

Here's the function serialize takes an archive object and passes class members to it for serialization.

save and load functions

When you need to divide the serialization process into loading and saving, you can use the functions save And load. Must-have when you need to perform additional tasks. Actions when loading or saving data:

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

Function save must be constbecause it should not change the state of the object.

Save_minimal and load_minimal functions

These functions allow you to minimize the output by representing an object as a single primitive or string. Useful for simplifying human-readable archives:

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

Smart pointers

Cereal supports serialization of smart pointers std::shared_ptr And std::unique_ptr. This way you can serialize objects referenced by smart pointers without any extra effort:

#include <cereal/types/memory.hpp>

struct DataHolder {
    std::shared_ptr<MyRecord> record;

    template <class Archive>
    void serialize(Archive& ar) {
        ar(record);
    }
};

Inheritance

There are functions cereal::base_class And cereal::virtual_base_classwhich help to correctly serialize base and derived classes:

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

Archives and their types

Cereal supports several types of archives: binary, XML and JSON archives. Each of them is used to serialize data in different formats.

Binary archives

#include <cereal/archives/binary.hpp>

std::ofstream os("data.cereal", std::ios::binary);
cereal::BinaryOutputArchive archive(os);
archive(someData);

XML archives

#include <cereal/archives/xml.hpp>

std::ofstream os("data.xml");
cereal::XMLOutputArchive archive(os);
archive(someData);

JSON archives

#include <cereal/archives/json.hpp>

std::ofstream os("data.json");
cereal::JSONOutputArchive archive(os);
archive(someData);

Type versioning

There is a macro for managing type versions CEREAL_CLASS_VERSIONwhich allows you to set the version for each data type:

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

Examples of using

Saving and loading application configuration

Cereal is often used to serialize configuration files. Let's look at an example where the application configuration is stored in a JSON file, and we would like to save it and load it when the application starts:

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

Saving Game State

It would be nice for toys to save game state so the player can pick up where they left off. You can easily save and load game data using 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;
}

Serialization of data for network exchange

In distributed systems and network applications, you primarily need to serialize data for transmission over the network.

Example:

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

How can a C++ developer organize cross-platform development? Arseny Cherenkov will talk about this. Meet us at the free practical lesson “Conan Package Manager for C++ Projects” from OTUS.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *