Domain Track
Difficulty 2/5Serialization in C++
C++ serialization patterns — manual binary, nlohmann/json, Protocol Buffers, MessagePack, std::from_chars for fast parsing, and versioning strategies.
TL;DR
C++ has no built-in serialization. Choose based on: human-readable (JSON), schema-enforced (Protobuf/Flatbuffers), or fast binary (manual/MessagePack). Always version your binary formats.
Manual Binary Serialization
Zero-copy, maximum control:
cpp
#include <cstring>
#include <vector>
// Simple buffer writer/reader
class BinaryWriter {
std::vector<std::byte> buf_;
public:
template<typename T>
void write(const T& v) {
static_assert(std::is_trivially_copyable_v<T>);
auto* ptr = reinterpret_cast<const std::byte*>(&v);
buf_.insert(buf_.end(), ptr, ptr + sizeof(T));
}
void write_string(std::string_view s) {
write(static_cast<uint32_t>(s.size()));
auto* ptr = reinterpret_cast<const std::byte*>(s.data());
buf_.insert(buf_.end(), ptr, ptr + s.size());
}
std::span<const std::byte> data() const { return buf_; }
};
class BinaryReader {
const std::byte* ptr_;
const std::byte* end_;
public:
BinaryReader(std::span<const std::byte> data)
: ptr_{data.data()}, end_{data.data() + data.size()} {}
template<typename T>
T read() {
if (ptr_ + sizeof(T) > end_) throw std::out_of_range("read past end");
T v;
std::memcpy(&v, ptr_, sizeof(T));
ptr_ += sizeof(T);
return v;
}
std::string read_string() {
auto len = read<uint32_t>();
if (ptr_ + len > end_) throw std::out_of_range("string too long");
std::string s{reinterpret_cast<const char*>(ptr_), len};
ptr_ += len;
return s;
}
};
struct Packet {
uint32_t id;
float value;
std::string name;
void serialize(BinaryWriter& w) const {
w.write(id); w.write(value); w.write_string(name);
}
static Packet deserialize(BinaryReader& r) {
return {r.read<uint32_t>(), r.read<float>(), r.read_string()};
}
};JSON with nlohmann/json
The most popular C++ JSON library:
cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// Build JSON
json j;
j["name"] = "Alice";
j["age"] = 30;
j["active"] = true;
j["scores"] = {95, 88, 92};
j["address"] = {{"city", "New York"}, {"zip", "10001"}};
std::string s = j.dump(2); // pretty-print with indent=2
// Parse JSON
json parsed = json::parse(R"({"name": "Bob", "age": 25})");
std::string name = parsed["name"];
int age = parsed.at("age"); // throws if missing
// Structured bindings
auto [name2, age2] = parsed.items();
// Custom serialization via to_json/from_json
struct Person { std::string name; int age; };
void to_json(json& j, const Person& p) {
j = json{{"name", p.name}, {"age", p.age}};
}
void from_json(const json& j, Person& p) {
j.at("name").get_to(p.name);
j.at("age").get_to(p.age);
}
Person alice{"Alice", 30};
json jalice = alice; // to_json called automatically
Person bob = json::parse(R"({"name":"Bob","age":25})").get<Person>();Protocol Buffers (Protobuf)
Schema-enforced, strongly typed, efficient:
protobuf
// person.proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
repeated string emails = 3;
}cpp
// C++ generated code usage
#include "person.pb.h"
Person p;
p.set_name("Alice");
p.set_age(30);
p.add_emails("alice@example.com");
// Serialize to string
std::string bytes;
p.SerializeToString(&bytes);
// Deserialize
Person p2;
p2.ParseFromString(bytes);
std::println("{}", p2.name()); // AliceMessagePack (Fast Binary JSON)
Binary, schema-less, compact:
cpp
#include <msgpack.hpp>
struct Config {
std::string host;
int port;
MSGPACK_DEFINE(host, port) // macro generates serialize/deserialize
};
// Serialize
Config cfg{"localhost", 8080};
msgpack::sbuffer sbuf;
msgpack::pack(sbuf, cfg);
// Deserialize
auto handle = msgpack::unpack(sbuf.data(), sbuf.size());
auto obj = handle.get();
Config cfg2;
obj.convert(cfg2);Fast Number Parsing with from_chars
For custom text protocols where speed matters:
cpp
#include <charconv>
// Parse a CSV line: "1.5,2.7,3.9\n"
std::vector<double> parse_doubles(std::string_view line) {
std::vector<double> vals;
auto p = line.data();
auto end = line.data() + line.size();
while (p < end) {
double v;
auto [next, ec] = std::from_chars(p, end, v);
if (ec != std::errc{}) break;
vals.push_back(v);
p = next;
if (p < end && (*p == ',' || *p == '\n')) ++p;
}
return vals;
}Versioned Binary Format
cpp
struct FileHeader {
uint32_t magic; // 0x43504200 "CPB\0"
uint16_t version; // format version
uint16_t flags;
uint64_t data_size;
};
static_assert(sizeof(FileHeader) == 16);
class VersionedSerializer {
static constexpr uint32_t kMagic = 0x43504200;
static constexpr uint16_t kVersion = 2;
public:
void write(std::ostream& out, const MyData& data) {
FileHeader hdr{kMagic, kVersion, 0, 0};
// serialize data into buffer...
std::vector<std::byte> payload = serialize_payload(data);
hdr.data_size = payload.size();
out.write(reinterpret_cast<const char*>(&hdr), sizeof(hdr));
out.write(reinterpret_cast<const char*>(payload.data()), payload.size());
}
MyData read(std::istream& in) {
FileHeader hdr;
in.read(reinterpret_cast<char*>(&hdr), sizeof(hdr));
if (hdr.magic != kMagic) throw std::runtime_error("bad magic");
if (hdr.version > kVersion) throw std::runtime_error("version too new");
std::vector<std::byte> payload(hdr.data_size);
in.read(reinterpret_cast<char*>(payload.data()), hdr.data_size);
if (hdr.version == 1) return deserialize_v1(payload);
return deserialize_v2(payload);
}
};Library Comparison
| Library | Format | Schema | Speed | Use when |
|---|---|---|---|---|
nlohmann/json | JSON | No | Medium | Human-readable, debugging |
simdjson | JSON | No | Very fast | High-throughput JSON parsing |
| Protobuf | Binary | Required | Fast | Cross-language, versioned APIs |
| Flatbuffers | Binary | Required | Very fast | Zero-copy access |
| MessagePack | Binary | No | Fast | Binary JSON without schema |
| Manual binary | Custom | In code | Maximum | Embedded, protocols |
cereal | JSON/Binary/XML | In code | Good | C++ only, easy integration |