也谈Boost::Serialization的用途和用法

研究生只念一年的坏处就是毕业设计好像变成一年一度的了……这次毕设是和并行计算有关(毕竟念的是「高性能计算硕士」),多线程是不够的,因为一台设备的CPU核心数毕竟有限,所以多进程的并行计算才能发挥计算机集群(HPC cluster)的计算威力。这方面的de facto standard就是MPI了,而在C++项目中可以通过Boost库的MPI包装更方便、「更C++」的来调用。Boost库的质量和重要程度个人感觉仅次于STL了,看看C++11吸收了大量Boost库进入STL就知道Boost有多厉害。

和Serialization的关系?既然用C++,就免不了自定义类吧,想要把一个类的实例通过MPI发送到其他MPI节点上,首先就要把类进行serialize,然后把serialized memory发送出去,接收方再unpack还原成一个实例。就不展开说了,简而言之这一点和MPI的通信原理有关。

所以Serialization库的用途就是可以帮助把类(包括模板类)进行serialize,这样处理之后的类就能使用Boost库中其他的函数比如archive库来把类的实例进行输入、输出(想想游戏的读档、存档)。

简单示例

以我目前还在进行的毕业设计代码文件position.h为例:

#ifndef POSITION_H
#define POSITION_H

#include <array>
#include <boost/serialization/access.hpp>
#include <boost/mpi/datatype.hpp>

class position
{
public:
    position(int _file = 0, int _rank = 0);
    position(const position &b);
    int file;
    int rank;

    bool not_in_range(int min_file, int max_file, int min_rank, int max_rank) const;

    position& operator= (const position &b);
    position operator+ (const position &b);
    position operator- (const position &b);
    bool operator< (const position &b) const;

private:
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive &ar, const unsigned int version)
    {
        ar & file;
        ar & rank;
    }
};

inline bool operator !=(const position &a, const position &b)
{
    return (a.file != b.file) || (a.rank != b.rank);
}

inline bool operator ==(const position &a, const position &b)
{
    return !(a != b);
}

typedef std::array<position, 2> pos_move;//moving a piece from move[0] to move[1]
template<class Archive>
void serialize(Archive &ar, pos_move &p, const unsigned int version)
{
    ar & p[0];
    ar & p[1];
}

BOOST_IS_MPI_DATATYPE(position)
BOOST_IS_MPI_DATATYPE(pos_move)

#endif //POSITION_H

这个类是非常简单,只包含了基础数据类型(int, float, double, bool等,本例只有int)。把一个类进行serialize适配其实就是添加serialize这个特殊的函数,而为了让Archive能够访问私有成员,需要加上 friend class boost::serialization::access; 这一句。然后在private域写好 template void serialize(Archive &ar, const unsigned int version) 函数。操作符&相当于流操作符<<和>>的合体,在输出数据的时候就是<<,在输入的时候就是>>。

你也可以把serialize函数拆开,拆开写就不是serialize函数了,而是save和load两个函数,可以更加灵活地控制数据输入输出的操作。

最下面的pos_move是一个array的模板类,STL的类库当然不会加上Boost的serialize的支持,所以我们要手动适配。因为我们不能修改STL的头文件(呃,理论上可以,但是那样的话你就要ship一份你改过的标准库,何必呢?),所以适配函数是「非侵入式」的,也就是定义在类外面的。可以看到函数的prototype变长了,第二个参数是要serialize的类的引用,操作倒是基本一样的。

宏BOOST_IS_MPI_DATATYPE是告诉Boost这个类型的数据长度是固定的(没有用到动态长度的容器如list, vector等),Boost::MPI会对这样的类进行一些性能上的优化,以后有机会谈Boost::MPI的时候再细说。

衍生类

假设我以position为基类衍生了一个global_position类,那么衍生类也需要做类似的操作,适配好serialize函数,不过不同的地方在于serialize函数体:

private:
    int offset;

    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive &ar, const unsigned int version)
    {
        ar & boost::serialization::base_object<position>(*this);
        ar & offset;
    }

首行是让Boost调用基类的serialize函数,而offset是global_position唯一新增成员数据,所以需要显式地处理一下。

另外一个容易踩进去的坑是忘记register或export,然后使用了基类指针,忘记的结果是编译时通过,运行时当需要serialize基类指针的时候抛出异常。这个有点冗长,就不在这里说了,可以看看官方文档的说明。如果你压根就不使用指针,就不用担心这个问题了(但是不用指针如何实现多态呢?不需要多态的话,类的继承衍生又没很大用处了)。

STL容器

如果使用了STL的容器,比如list、vector等,只需要包含对应的serialization头文件即可,并不需要做特别地处理。如:

private:
    std::list<position> pos_list;

    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive &ar, const unsigned int version)
    {
        ar & pos_lis;
    }

你只需要包含好头文件#include <boost/serialization/list.hpp>即可。

参考文章: Boost::Serialization

2 responses to “也谈Boost::Serialization的用途和用法”

  1. 薛定谔的茶几 avatar
    薛定谔的茶几

    量子化 量子化!

    1. librehat avatar

      atomic应该尽量用STL,C++11的标准要求了很多东西,Boost放宽了,但是Boost的atomic没有STL的强大。过段时间我应该会写一篇关于std::atomic的文章,毕竟这也是个折腾了我几天的部分。