Serializing data in C++ with the Cereal library
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 const
because 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_class
which 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_VERSION
which 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.