从样例出发了解序列库cereal的实现

从样例出发了解序列库cereal的实现

本文通过打断点F11单步调试验证:参考:从样例出发了解序列库cereal的实现

参考代码以cereal::BinaryOutputArchive为切入点,也是本文,后续发现以cereal::XMLOutputArchive为切入点更加清晰。

Cereal库简介

参考 https://uscilab.github.io/cereal/

Cereal是一个只有头文件形式的C++11序列化库,可以对任意数据类型进行序列化处理,并且能反向将序列化的数据解析成不同的二进制编码形式,如XML或者JSON。其设计目标是快速、轻量和易于扩展。

  • 支持序列化的类型:Cereal基本支持每种标准库类型,如std::vectorstd::liststd::mapstd::shared_ptrstd::unique_ptr等,还支持继承和多态类的序列化。但是,裸指针和引用的序列化是不支持的。
  • 支持的编译器:支持g++4.7.3、clang++3.3和MSVC2013(或者更新的版本),可能支持老版本的编译器,但不保证。
  • 性能:简单的性能测试显示,Cereal通常比Boost的序列化库更快,生成的二进制序列占用更少的空间。
  • 代码易理解性:代码相对于Boost易于理解。
  • 序列化格式:提供了binary、XML、JSON格式的序列化类型,也可扩展其他类型的打包方法(archive)和类型。
  • 代码质量:代码由单元测试覆盖,代码质量得到了一定的保障。

本文的目标

通过样例代码的深入解读,一窥cereal底层细节,期望获得某种程度的理解。

样例分析

以下样例代码取自于Cereal的GitHub主页,代码功能是序列化结构体SomeData的成员data,以二进制形式保存到文件out.cereal中。其中,模板成员函数saveload及结构体MyRecord的模板成员函数serialize是Cereal序列化的典型接口。

Cereal序列化库中宏递归展开方案

未优化:参考nlohmann json设计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>

编译期,编译器会特化出为模板参数为BinaryOutputArchiveAllowEmptyClassElision的具体类的定义。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)
      { }
  1. 保存子类实例的指针this
  2. 下一个指针类Id itsCurrentPointerId,在其成员函数registerSharedPointer中用到
auto ptrId = itsCurrentPointerId++;
itsSharedPointerMap.insert( {addr, ptrId} );//Maps from addresses to pointer ids从地址到指针标识的映射表
  1. 隐式构造了std::unordered_map<void const *, std::uint32_t>类型变量 itsSharedPointerMap 从地址到指针标识的映射表
  2. itsCurrentPolymorphicTypeId保存下一个多态类id,在其成员函数registerPolymorphicType中用到
auto polyId = itsCurrentPolymorphicTypeId++;
itsPolymorphicTypeMap.insert( {name, polyId} );//Maps from polymorphic type name strings to ids将多态类型名称字符串映射到标识符的映射表
  1. 隐式构造了std::unordered_map<char const *, std::uint32_t>类型变量itsPolymorphicTypeMap 将多态类型名称字符串映射到标识符的映射表
  2. 要被序列化的所有基类的集合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;
  1. 用来追踪类型版本信息的集合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_setstd::unordered_map而不是std::setstd::map,想必是出于性能的考虑。std::unordered_setstd::unordered_map是基于散列表实现的,读取时间复杂度是O(1);而std::setstd::map底层是红黑树的实现,读取的复杂度在O(logN)。显然,std::unordered_setstd::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 用与检测成员函数选择执行函数的实现解释:

利用SFINAE检测成员函数

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;

智能指针类型数据dataarchive()

智能指针archive调用仿函数archive扩展和SomeData类型的相似,不同点在于:

  1. 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;
  }
  1. 选择已实现的非成员函数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,一直访问到基本数据类型并序列化,层层递归,最终得到序列化的字符串。代码设计上,大量运用到模板,设计之精巧,让人叹为观止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值