Ещё одна сериализация для C++

Сериализация и десериализация переменных и объектов — процедура настолько частая, что, сохраняя что-то вычисленное на диске, записывая вывод программы в текстовый файл или отдавая в сетевой интерфейс, мы даже не думаем, что мы это сериализуем.
Хотя инструментов для сериализации существует достаточно много, я предлагаю вашему вниманию ещё один. Он не лучше и не хуже других, и был создан с акцентом на простоту (кто бы мог подумать?) и компактность (опять же!), не сильно влияющую на производительность работы с ранее сериализованными данными.
Привет! Я Андрей Коваленко (Keva). Я занимаюсь лингвистическими задачами и делаю поисковые движки. Первый Апорт!, движок Rambler образца 2000 года и украинская — мои разработки. Недавно завершил работу над большой корпоративной поисковой системой МойОфис.
Поскольку я активно строю и использую словари того или иного вида -, а оглавление поискового индекса по сути своей тоже есть словарь, задачи сериализации, десериализации и работы с сериализованными данными без их явной десериализации решаю постоянно. Так что неудивительно, что сериализатор был выделен в отдельную библиотеку — один заголовочный файл.
Инструмент исполнен как набор шаблонных методов, поддерживающих наиболее распространённые типы и классы данных, и может дополняться в коде проекта короткими определениями для отправки результатов в тот или иной коллектор. Всего декларируются 4 шаблонных метода в глобальном пространстве имён, где T — сериализуемый тип, O — коллектор, а s — источник:
template size_t GetBufLen( const T& );
template O* Serialize( O*, const T& );
template S* FetchFrom( S*, T& );
template 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 O* Serialize( O* o ) const
{
return ::Serialize( ::Serialize( o, i ), s );
}
template 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 байта. На круг получается компактнее.
Типизация в коллекторах не предусмотрена — это ответственность самой программы — знать, что читать. Поэтому при желании можно сериализовать std: vector
Про сериализацию данных произвольной структуры, в том числе и потенциально неполных, на днях напишу отдельно.
Ну, а код доступен на github.
Habrahabr.ru прочитано 18578 раз