从样例出发了解序列库cereal的实现
本文通过打断点F11单步调试验证:参考:从样例出发了解序列库cereal的实现
参考代码以
cereal::BinaryOutputArchive
为切入点,也是本文,后续发现以cereal::XMLOutputArchive
为切入点更加清晰。
Cereal库简介
参考 https://uscilab.github.io/cereal/
Cereal是一个只有头文件形式的C++11序列化库,可以对任意数据类型进行序列化处理,并且能反向将序列化的数据解析成不同的二进制编码形式,如XML或者JSON。其设计目标是快速、轻量和易于扩展。
- 支持序列化的类型:Cereal基本支持每种标准库类型,如
std::vector
、std::list
、std::map
、std::shared_ptr
和std::unique_ptr
等,还支持继承和多态类的序列化。但是,裸指针和引用的序列化是不支持的。 - 支持的编译器:支持g++4.7.3、clang++3.3和MSVC2013(或者更新的版本),可能支持老版本的编译器,但不保证。
- 性能:简单的性能测试显示,Cereal通常比Boost的序列化库更快,生成的二进制序列占用更少的空间。
- 代码易理解性:代码相对于Boost易于理解。
- 序列化格式:提供了binary、XML、JSON格式的序列化类型,也可扩展其他类型的打包方法(archive)和类型。
- 代码质量:代码由单元测试覆盖,代码质量得到了一定的保障。
本文的目标
通过样例代码的深入解读,一窥cereal底层细节,期望获得某种程度的理解。
样例分析
以下样例代码取自于Cereal的GitHub主页,代码功能是序列化结构体SomeData
的成员data
,以二进制形式保存到文件out.cereal
中。其中,模板成员函数save
、load
及结构体MyRecord
的模板成员函数serialize
是Cereal序列化的典型接口。
Cereal序列化库中宏递归展开方案
#include <cereal/types/unordered_map.hpp>
#include <cereal/types/memory.hpp>
#include <cereal/archives/binary.hpp>
#include <fstream>
struct MyRecord
{
uint8_t x, y;
float z;
template <class Archive>
void serialize( Archive & ar )
{
ar( x, y, z );
}
};
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 );
}
};
int main()
{
std::ofstream os("out.cereal", std::ios::binary);
cereal::BinaryOutputArchive archive( os );
SomeData myData;
archive( myData );
return 0;
}
cereal::BinaryOutputArchive
类型实例archive
的构造
BinaryOutputArchive
继承于OutputArchive<BinaryOutputArchive, AllowEmptyClassElision>
。
class BinaryOutputArchive : public OutputArchive<BinaryOutputArchive, AllowEmptyClassElision>
编译期,编译器会特化出为模板参数为BinaryOutputArchive
和AllowEmptyClassElision
的具体类的定义。BinaryOutputArchive
构造过程包括两个方面的初始化:
- 初始化成员
std::ostream
类型变量itsStream
- 构造特化的父类对象,将
this
指针传给父类
cereal\archives\binary.hpp
BinaryOutputArchive(std::ostream & stream) :
OutputArchive<BinaryOutputArchive, AllowEmptyClassElision>(this),
itsStream(stream)
{ }
进一步,OutputArchive<BinaryOutputArchive, AllowEmptyClassElision>
类型实例的构造,包括以下多个成员的实例化:
cereal\cereal.hpp
template<class ArchiveType, std::uint32_t Flags = 0>
class OutputArchive : public detail::OutputArchiveBase
{
public:
OutputArchive(ArchiveType * const derived) : self(derived), itsCurrentPointerId(1), itsCurrentPolymorphicTypeId(1)
{ }
- 保存子类实例的指针
this
- 下一个指针类Id
itsCurrentPointerId
,在其成员函数registerSharedPointer
中用到
auto ptrId = itsCurrentPointerId++;
itsSharedPointerMap.insert( {addr, ptrId} );//Maps from addresses to pointer ids从地址到指针标识的映射表
- 隐式构造了
std::unordered_map<void const *, std::uint32_t>
类型变量itsSharedPointerMap
从地址到指针标识的映射表 itsCurrentPolymorphicTypeId
保存下一个多态类id,在其成员函数registerPolymorphicType
中用到
auto polyId = itsCurrentPolymorphicTypeId++;
itsPolymorphicTypeMap.insert( {name, polyId} );//Maps from polymorphic type name strings to ids将多态类型名称字符串映射到标识符的映射表
- 隐式构造了
std::unordered_map<char const *, std::uint32_t>
类型变量itsPolymorphicTypeMap
将多态类型名称字符串映射到标识符的映射表 - 要被序列化的所有基类的集合
itsBaseClassSet
//A set of all base classes that have been serialized所有已序列化的基类的集合
std::unordered_set<traits::detail::base_class_id, traits::detail::base_class_id_hash> itsBaseClassSet;
- 用来追踪类型版本信息的集合
std::unordered_set<size_type> itsVersionedTypes
//Keeps track of classes that have versioning information associated with them记录了那些具有版本信息关联的课程信息
std::unordered_set<size_type> itsVersionedTypes;
这里运用到了std::unordered_set
和std::unordered_map
而不是std::set
和std::map
,想必是出于性能的考虑。std::unordered_set
和std::unordered_map
是基于散列表实现的,读取时间复杂度是O(1);而std::set
和std::map
底层是红黑树的实现,读取的复杂度在O(logN)。显然,std::unordered_set
和std::unordered_map
性能更优。
仿函数archive(myData)
这里调用到operator()(...)
运算符重载,即仿函数(functor),该可变参数模板形式的仿函数,可以适应任意数量和类型的参数,通过std::forward
将入参保持值类型 传递给成员函数process
。
值类型:左/右值引用类型 等
值类别:纯右值 (prvalue)、亡值 (xvalue)、左值 (lvalue)。
cereal\cereal.hpp的331行 class OutputArchive 模板类中
//! Serializes all passed in data 将所有传入的数据进行序列化处理
/*! This is the primary interface for serializing data with an archive
这是用于通过存档对数据进行序列化的主要界面。*/
template <class ... Types> inline
ArchiveType & operator()( Types && ... args )
{
self->process( std::forward<Types>( args )... );
return *self;
}
SomeData myData;
archive(myData);
编译期,类型SomeData
,可以扩展为self->process(std::forward<SomeData>(myData))
,继续扩展void process( T && head, Other && ... tail )
–>self->processImpl( head )
cereal\cereal.hpp的438行 class OutputArchive 模板类中
//! Serializes data after calling prologue, then calls epilogue
//在调用前序代码后对数据进行序列化处理,然后调用后序代码。
template <class T> inline
void process( T && head )
{
prologue( *self, head )
self->processImpl( head );
epilogue( *self, head );
}
//! Unwinds to process all data 将数据全部解压缩处理
template <class T, class ... Other> inline
void process( T && head, Other && ... tail )
{
self->process( std::forward<T>( head ) );
self->process( std::forward<Other>( tail )... );
}
之后,traits PROCESS_IF
出场,因为SomeData
具有save
的成员函数,于是,编译器会选择到类成员函数processImpl
PROCESS_IF
用与检测成员函数选择执行函数的实现解释:
cereal\cereal.hpp的497行 class OutputArchive 模板类中
//! Helper macro that expands the requirements for activating an overload
/*! Requirements:
Has the requested serialization function
Does not have version and unversioned at the same time
Is output serializable AND
is specialized for this type of function OR
has no specialization at all */
/*这是一个宏助手,用于展开激活重载所需的条件!
要求如下:
- 具有所需的序列化函数
- 不同时具有版本化和非版本化的函数
- 是可输出序列化的,并且
- 为这种类型的函数进行了特化,或者
- 根本没有进行特化
*/
#define PROCESS_IF(name) \
traits::EnableIf<traits::has_##name<T, ArchiveType>::value, \
!traits::has_invalid_output_versioning<T, ArchiveType>::value, \
(traits::is_output_serializable<T, ArchiveType>::value && \
(traits::is_specialized_##name<T, ArchiveType>::value || \
!traits::is_specialized<T, ArchiveType>::value))> = traits::sfinae
//! Member split (save)成员分离(保存)
template <class T, PROCESS_IF(member_save)> inline
ArchiveType & processImpl(T const & t)
{
access::member_save(*self, t);
return *self;
}
继续展开access::member_save()
,则找到access
类静态模板成员函数,CEREAL_SAVE_FUNCTION_NAME
即为save
,即最终调用到SomeData
内部的save
模板成员函数。
cereal\macros.hpp中 88行
#ifndef CEREAL_SAVE_FUNCTION_NAME
//! The serialization (save) function name to search for.
/*! You can define @c CEREAL_SAVE_FUNCTION_NAME to be different assuming you do so
before this file is included. */
/*
待搜索的序列化(保存)函数名称。
你可以定义 @c CEREAL_SAVE_FUNCTION_NAME 为不同的值,前提是必须在包含此文件之前进行定义。
*/
#define CEREAL_SAVE_FUNCTION_NAME save
#endif // CEREAL_SAVE_FUNCTION_NAME
cereal\access.hpp 中 249行 class access 类中
template<class Archive, class T> inline
static auto member_save(Archive & ar, T const & t) -> decltype(t.CEREAL_SAVE_FUNCTION_NAME(ar))
{ return t.CEREAL_SAVE_FUNCTION_NAME(ar); }
此时t
的类型是SomeData const&
,调用此类型的模板成员函数save
,传入data
std::shared_ptr<std::unordered_map<uint32_t, MyRecord>> data;
智能指针类型数据data
的archive()
智能指针archive
调用仿函数archive
扩展和SomeData
类型的相似,不同点在于:
-
processImpl
会找到非成员函数模板cereal\cereal.hpp的530行 class OutputArchive 模板类中
//! Non member split (save)非成员分割(保存)
template <class T, PROCESS_IF(non_member_save)> inline
ArchiveType & processImpl(T const & t)
{
CEREAL_SAVE_FUNCTION_NAME(*self, t);
return *self;
}
- 选择已实现的非成员函数
save
代码如下,这里生成对应的id
,缓存id
,序列化("id", id)
,之后序列化指针指向的内容("data", *ptr)
NVP 即
template <class T>
class NameValuePair
include\cereal\types\memory.hpp 中262行
//! Saving std::shared_ptr (wrapper implementation)
/*! @internal */
template <class Archive, class T> inline
void CEREAL_SAVE_FUNCTION_NAME( Archive & ar, memory_detail::PtrWrapper<std::shared_ptr<T> const &> const & wrapper )
{
auto & ptr = wrapper.ptr;
uint32_t id = ar.registerSharedPointer( ptr );
ar( CEREAL_NVP_("id", id) );
if( id & detail::msb_32bit )
{
ar( CEREAL_NVP_("data", *ptr) );
}
}
std::unordered_map 类型ar( CEREAL_NVP_(“data”, *ptr) )
跳过ar( CEREAL_NVP_("id", id) )
,继续往内部挖掘ar( CEREAL_NVP_("data", *ptr) )
,类似的扩展,但是走到std::unordered_map
对应的save
模板函数
cereal\types\concepts\pair_associative_container.hpp
std::unordered_map<uint32_t, MyRecord>
template <class Archive, template <typename...> class Map, typename... Args, typename = typename Map<Args...>::mapped_type> inline
void CEREAL_SAVE_FUNCTION_NAME( Archive & ar, Map<Args...> const & map )
{
ar( make_size_tag( static_cast<size_type>(map.size()) ) );
for( const auto & i : map )
ar( make_map_item(i.first, i.second) );
}
深入make_map_item -> MapItem<KeyType, ValueType>
cereal\details\helpers.hpp 中 351 行
template <class Key, class Value>
struct MapItem
{
using KeyType = typename std::conditional<
std::is_lvalue_reference<Key>::value,
Key,
typename std::decay<Key>::type>::type;
using ValueType = typename std::conditional<
std::is_lvalue_reference<Value>::value,
Value,
typename std::decay<Value>::type>::type;
//! Construct a MapItem from a key and a value 从键和值构造一个MapItem。
/*! @internal 内部使用*/
MapItem( Key && key_, Value && value_ ) : key(std::forward<Key>(key_)), value(std::forward<Value>(value_)) {}
MapItem & operator=( MapItem const & ) = delete;
KeyType key;
ValueType value;
//! Serialize the MapItem with the NVPs "key" and "value" 使用名称值对(NVPs)“key”和“value”序列化MapItem。
template <class Archive> inline
void CEREAL_SERIALIZE_FUNCTION_NAME(Archive & archive)
{
archive( make_nvp<Archive>("key", key),
make_nvp<Archive>("value", value) );
}
}
基本类型的序列化
std::ofstream os("out.cereal", std::ios::binary);
cereal::BinaryOutputArchive archive(os);
//SomeData myData;
MyRecord myData;
archive(myData);
F11 调试 一步一步深入,最终会调用到MyRecord
函数serialize
,进一步的是到基本类型的序列化
cereal\archives\binary.hpp 中 115行
//! Saving for POD types to binary 为POD类型进行二进制保存
template<class T> inline
typename std::enable_if<std::is_arithmetic<T>::value, void>::type
CEREAL_SAVE_FUNCTION_NAME(BinaryOutputArchive & ar, T const & t)
{
ar.saveBinary(std::addressof(t), sizeof(t));
}
cereal\archives\binary.hpp 中 65行
//! Writes size bytes of data to the output stream 将数据的size字节写入输出流
void saveBinary( const void * data, std::streamsize size )
{
auto const writtenSize = itsStream.rdbuf()->sputn( reinterpret_cast<const char*>( data ), size );
if(writtenSize != size)
throw Exception("Failed to write " + std::to_string(size) + " bytes to output stream! Wrote " + std::to_string(writtenSize));
}
STL容器序列化
cereal/types/list.hpp
cereal/types/vector.hpp
cereal/types/map.hpp
->
cereal\types\concepts\pair_associative_container.hpp
总结
本文大致对Cereal序列化内部调用进行了扩展。算法上,代码实现了数据结构深度遍历的过程,出发节点是myData
,一直访问到基本数据类型并序列化,层层递归,最终得到序列化的字符串。代码设计上,大量运用到模板,设计之精巧,让人叹为观止。