Сериализация и десериализация переменных и объектов - процедура настолько частая, что, сохраняя что-то вычисленное на диске, записывая вывод программы в текстовый файл или отдавая в сетевой интерфейс, мы даже не думаем, что мы это сериализуем.
Хотя инструментов для сериализации существует достаточно много, я предлагаю вашему вниманию ещё один. Он не лучше и не хуже других, и был создан с акцентом на простоту (кто бы мог подумать?) и компактность (опять же!), не сильно влияющую на производительность работы с ранее сериализованными данными.
Привет! Я Андрей Коваленко (Keva). Я занимаюсь лингвистическими задачами и делаю поисковые движки. Первый Апорт!, движок Rambler образца 2000 года и украинская <META> - мои разработки. Недавно завершил работу над большой корпоративной поисковой системой МойОфис.
Поскольку я активно строю и использую словари того или иного вида - а оглавление поискового индекса по сути своей тоже есть словарь, задачи сериализации, десериализации и работы с сериализованными данными без их явной десериализации решаю постоянно. Так что неудивительно, что сериализатор был выделен в отдельную библиотеку - один заголовочный файл.
Инструмент исполнен как набор шаблонных методов, поддерживающих наиболее распространённые типы и классы данных, и может дополняться в коде проекта короткими определениями для отправки результатов в тот или иной коллектор. Всего декларируются 4 шаблонных метода в глобальном пространстве имён, где T - сериализуемый тип, O - коллектор, а s - источник:
template <class T> size_t GetBufLen( const T& );
template <class O,
class T> O* Serialize( O*, const T& );
template <class S,
class T> S* FetchFrom( S*, T& );
template <class S,
class T> S* SkipToEnd( S*, const T* );
GetBufLen( ... )
возвращает количество байт, которое потребует сериализация переданного объекта - например, для резервирования места или построения релокаций в сериализованных данных, если такие нужны.
Serialize( O* o, const T& t )
записывает объект t в коллектор, типизированный указатель на который передаётся первым параметром.
FetchFrom( S* s, T& t )
извлекает из источника s объект t.
SkipToEnd( S* s, const T* )
проматывает объект заданного типа T без его создания, переходя к следующему сериализованному объекту. В данном случае T* используется лишь для указания типа, и обычное его использование - передача (const T*)nullptr.
Для накопителей "память (char*)" и "c-style файл (FILE*)" примитивы заданы сразу:
template <> auto Serialize( char* o, const void* p, size_t l ) -> char*
{ return o != nullptr ? l + (char*)memcpy( o, p, l ) : nullptr; }
template <> auto Serialize( FILE* o, const void* p, size_t l ) -> FILE*
{ return o != nullptr && fwrite( p, sizeof(char), l, o ) == l ? o : nullptr; }
template <> auto FetchFrom( const char* s, void* p, size_t l ) -> const char*
{ return s != nullptr ? (memcpy( p, s, l ), l + s) : nullptr; }
template <> auto FetchFrom( FILE* s, void* p, size_t l ) -> FILE*
{ return s != nullptr && fread( p, sizeof(char), l, s ) == l ? s : nullptr; }
template <> auto SkipBytes( const char* s, size_t l ) -> const char*
{ return s != nullptr ? s + l : s; }
template <> auto SkipBytes( FILE* s, size_t l ) -> FILE*
{ return s != nullptr && fseek( s, l, SEEK_CUR ) == 0 ? s : nullptr; }
Если требуется определить другой накопитель или источник, достаточно в собственном проекте доопределить тройку примитивов сериализации, чтения и прыжка-через. Например, чтобы отправить объект в пустоту, определим эту самую пустоту и метод работы с ней:
class blackhole {};
template <> auto Serialize( blackhole* o, const void*, size_t ) -> blackhole*
{ return o; }
Библиотека содержит примитивы кросплатформенной записи и чтения базовых типов (за исключением float и double - они пишутся as is), а также многих классов из стандартной библиотеки - vector, string, map, list, pair, tuple. Для собственных классов достаточно создать одноимённые методы, и они также будут успешно обрабатываться:
class storable
{
int i;
std::string s;
public:
template <class O> O* Serialize( O* o ) const
{
return ::Serialize( ::Serialize( o, i ), s );
}
template <class S> S* FetchFrom( S* s )
{
return ::FetchFrom( ::FetchFrom( s, i ), this->s );
}
};
...
storable object;
::Serialize( stdout, object );
Целочисленные значения от двух байт (uint16_t и выше) сериализуются с базовой компрессией - порциями по 7 бит для ненулевой части бит. То есть uint32_t(127) займёт 1 байт, а 128 уже два, а вот 65535 (16 значимых бит) уже 3 байта. На круг получается компактнее. Так, результатом сериализации вектора из пяти тридцатидвухбитных целых в примере ниже будет семибайтная последовательность:
auto my_vec = std::vector<uint32_t>{ 10, 20, 70, 100, 130 };
char serial[0x100];
auto endptr = ::Serialize( serial, my_vec );
fprintf( stdout, "array of %u bytes: [", unsigned(endptr - serial) );
for ( auto prefix = "", p = (const char*)serial; p != endptr; ++p, prefix = ", " )
fprintf( stdout, "%s\\x%02x", prefix, (uint8_t)*p );
fputs( "]\n", stdout );
array of 7 bytes: [\x05, \x0a, \x14, \x46, \x64, \x82, \x01]
В ней первый байт - размерность сериализованного вектора (5) и значения 10, 20, 70 и 100 занимают по 1 байту, а 130 (0x0102 = 128 + 2) записано двумя - младшим байтом (2 с флагом продолжения 0x80) и старшим (0x01).
Типизация в коллекторах не предусмотрена - это ответственность самой программы - знать, что читать. Поэтому при желании можно сериализовать std::vector<int>, а десериализовать std::list<int>; сериализовать const char[] = "...", а десериализовать std::string.
Про сериализацию данных произвольной структуры, в том числе и потенциально неполных, на днях напишу отдельно.
Ну а код доступен на github.