Какие плюсы и минусы у двоичной десериализации в C++?

«Какие плюсы и минусы у двоичной десериализации в C++?» — вопрос из категории Архитектура, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Двоичная (бинарная) сериализация/десериализация подразумевает прямую запись и чтение бинарного представления объектов в память или файл.

Плюсы:

  1. Высокая скорость: Отсутствие затрат на парсинг текстовых форматов (JSON, XML). Данные копируются или читаются напрямую.
  2. Компактность: Бинарное представление обычно занимает значительно меньше места, чем текстовое (нет имён полей, скобок, кавычек).
  3. Сохранение точности: Прямая запись бинарных данных типов с плавающей точкой (float, double) без потери точности из-за преобразования в строку и обратно.
  4. Поддержка сложных структур: Можно сериализовать упакованные структуры (POD-типы), битовые поля, массивы фиксированного размера.

Минусы:

  1. Отсутствие переносимости (Portability):
    • Endianness (порядок байт): Данные, записанные на little-endian системе (x86), будут некорректно прочитаны на big-endian системе (некоторые ARM, PowerPC) без преобразования.
    • Размер и выравнивание типов: int может быть 16, 32 или 64 бита в зависимости от платформы и компилятора. Выравнивание полей в структуре (padding) может различаться.
    • Структура классов: Не-POD типы (с виртуальными функциями, указателями) обычно нельзя сериализовать простым копированием памяти.
  2. Хрупкость формата: Любое изменение в структуре данных (порядок, тип или размер полей) ломает обратную совместимость. Требуется версионирование и миграция данных.
  3. Отсутствие человекочитаемости: Данные нельзя просмотреть или отредактировать в текстовом редакторе, сложнее отлаживать.
  4. Проблемы безопасности: Прямая загрузка данных в память может быть уязвима для атак, если данные повреждены или сформированы злоумышленником.

Пример сериализации POD-структуры с учётом выравнивания:

#include <cstdint>
#include <fstream>
#include <cstring>

// #pragma pack отключает выравнивание для переносимости.
#pragma pack(push, 1) // Гарантируем, что структура упакована без паддинга.
struct SerializableData {
    std::int32_t id;      // Используем типы фиксированного размера.
    double value;
    char tag[16];         // Массив фиксированного размера вместо std::string.
};
#pragma pack(pop)

bool writeData(const SerializableData& data, const std::string& filename) {
    std::ofstream file(filename, std::ios::binary);
    if (!file) return false;
    file.write(reinterpret_cast<const char*>(&data), sizeof(data));
    return file.good();
}

bool readData(SerializableData& data, const std::string& filename) {
    std::ifstream file(filename, std::ios::binary);
    if (!file) return false;
    file.read(reinterpret_cast<char*>(&data), sizeof(data));
    return file.good();
}

// Важно: Этот код всё ещё не переносим между системами с разным endianness!
// Для переносимости нужно преобразовывать многобайтовые типы (id, value) в сетевой порядок байт (htonl/ntohl и т.д.).