65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 序列化框架/库

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (4投票s)

2016年6月21日

CPOL

23分钟阅读

viewsIcon

20773

downloadIcon

350

紧凑型库中的保存、加载和数据迁移

下载 Serialization_v0.1.zip

引言

我需要在我的应用程序中集成一个序列化机制(数据的保存和加载),但我未能找到满足我要求的合适库。我主要关注 boost.serialization 库,它在我此处介绍的这个库的设计中也发挥了重要作用。在我的应用程序中,我为所有对象使用自定义分配器,因此无法将其与 boost.serialization 代码集成。然后我决定编写自己的库来满足我的所有要求。

背景

我使用 MFC 序列化(CObject::Serialize)很长时间了,但我不想将 MFC 集成到我的代码中,所以这不在我的选择范围内。来自 https://boost.ac.cn/doc/libs/1_61_0/libs/serialization/doc/ 的目标大多适用于我的应用程序,但并非全部(例如可移植性)。在我的库中,我专注于以下目标:

  1. 没有模板的序列化方法/函数 - 我需要从 DLL 导出序列化代码,而使用模板只会使其更困难
  2. 自定义内存管理(任意的,不仅是自定义分配器,还包括类 new/delete 运算符)
  3. 每个类定义的独立版本控制。也就是说,当类定义更改时,旧文件仍然可以导入到新版本的类中。
  4. 深度指针保存和恢复。也就是说,保存和恢复指针会保存和恢复所指向的数据。
  5. 正确恢复指向共享数据的指针。
  6. STL 容器和其他常用模板的序列化。
  7. 非侵入式。允许将序列化应用于未更改的类。也就是说,不需要要序列化的类派生自特定的基类或实现指定的成员函数。这对于轻松地将序列化应用于我们无法或不想更改的类库中的类是必要的。
  8. 存档接口必须足够简单,以便于创建新类型的存档。
  9. 支持复杂的迁移(下文将详细介绍)

第3-8点复制自上面的 boost 链接。

我不会在本文中重点介绍 boost.serialization。我认为这个库非常棒,在许多情况下都很有用,并且它也有很棒的文档。我还要说,我不是 boost.serialization 的专家。我在代码中遇到的问题,我只是认为它们无法克服,而拥有自己的库是唯一的出路。最后,我认为我编写的库对任何人都会非常有用,我不想自己保留它。

构建库

该库使用 boostvisual leak detector

https://boost.ac.cn/

https://vld.codeplex.com/

从以上链接下载库,将其解压到硬盘驱动器上,编辑 StartVS.bat 文件以设置库的路径,并通过执行此批处理文件启动 Visual Studio。SerializationTests 项目需要构建 boost 测试框架库,但构建库本身不需要它。

使用代码

让我们从一个例子开始(这是附件 zip 文件中的例子,但已改编为单个文件)

// Serialization
#include <Serialization/Archive/BinaryOutArchive.h>
#include <Serialization/Archive/BinaryInArchive.h>
#include <Serialization/Archive/OutArchiveStdFunctors.h>
#include <Serialization/Archive/InArchiveStdFunctors.h>
#include <Serialization/File/MemoryBinaryOutFile.h>
#include <Serialization/File/MemoryBinaryInFile.h>
#include <Serialization/DeclareMacros.h>
#include <Serialization/ImplementMacros.h>

// std
#include <string>

using namespace Serialization;

/// class to serialize
class MyData
{
public:
    /// construction
    MyData()
    : m_Value(0)
    { }
    MyData(int value, const std::string& name)
    : m_Value(value)
    , m_Name(name)
    { }

    ~MyData()
    { }

    /// get/set value
    int GetValue() const        { return m_Value; }
    void SetValue(int value)    { m_Value = value; }

    /// get/set name
    const std::string& GetName() const        { return m_Name; }
    void SetName(const std::string& name)    { m_Name = name; }

private:
    int                m_Value;
    std::string        m_Name;
};

/// serialization routines
void Save(BinaryOutArchive& ar, const MyData& data, const int /*classVersion*/)
{
    ar << data.GetValue();
    ar << data.GetName();
}
void Load(BinaryInArchive& ar, MyData& data, const int /*classVersion*/)
{
    int value;
    std::string name;
    ar >> value;;
    ar >> name;
    data.SetValue(value);
    data.SetName(name);
}

/// enable serialization for MyData
DECLARE_TYPE_INFO_STRING_KEY(MyData);
IMPLEMENT_TYPE_INFO(MyData, "MyData", 0);
REGISTER_KEY_SERIALIZATION(BinaryInArchive, BinaryOutArchive, const char*);
REGISTER_CLASS_SERIALIZATION(BinaryInArchive, BinaryOutArchive, MyData);

//////////////////////////////////////////////////////////////////////////
void main()
{
    // save data
    void* pBuffer = nullptr;
    size_t bufferSize = 0;
    try
    {
        MyData data1(1, "MyData#1"), data2(2, "MyData#2"), data3(3, "MyData#3");
        MemoryBinaryOutFile fo(1024);
        BinaryOutArchive ao(fo);
        ao << data1 << data2 << data3;
        pBuffer = fo.Release(bufferSize);

        printf_s("Buffer created: %d\n", bufferSize);
        printf_s("\tData1: value = %d; name = %s\n", data1.GetValue(), data1.GetName().c_str());
        printf_s("\tData2: value = %d; name = %s\n", data2.GetValue(), data2.GetName().c_str());
        printf_s("\tData3: value = %d; name = %s\n", data3.GetValue(), data3.GetName().c_str());
    }
    catch(SerializationException& e)
    {
        printf_s("Save error: %s\n", e.what());
        return;
    }

    printf_s("-------------------------------------------\n");

    // load data
    try
    {
        MyData data1, data2, data3;
        MemoryBinaryInFile fi(pBuffer, bufferSize);
        BinaryInArchive ai(fi);
        ai >> data1 >> data2 >> data3;

        printf_s("Loaded data:\n");
        printf_s("\tData1: value = %d; name = %s\n", data1.GetValue(), data1.GetName().c_str());
        printf_s("\tData2: value = %d; name = %s\n", data2.GetValue(), data2.GetName().c_str());
        printf_s("\tData3: value = %d; name = %s\n", data3.GetValue(), data3.GetName().c_str());
    }
    catch(SerializationException& e)
    {
        printf_s("Load error: %s\n", e.what());
    }

    // release
    free(pBuffer);
    bufferSize = 0;
}

用户类的序列化通过模板的特化实现。为了简化任务,库提供了一组宏来生成所需的特化。在上面的例子中,这些宏在这里使用

/// enable serialization for MyData
DECLARE_TYPE_INFO_STRING_KEY(MyData);
IMPLEMENT_TYPE_INFO(MyData, "MyData", 0);
REGISTER_KEY_SERIALIZATION(BinaryInArchive, BinaryOutArchive, const char*);
REGISTER_CLASS_SERIALIZATION(BinaryInArchive, BinaryOutArchive, MyData);

DECLARE_TYPE_INFO_STRING_KEY 定义了与类绑定的键的类型。序列化指向类的指针需要该键。存档需要存储序列化指针的类型信息,以便以后能够准确创建存储的类型并将其数据加载到其中。宏 DECLARE_TYPE_INFO_STRING_KEY 将字符串键类型与类绑定。库通过通用宏 DECLARE_TYPE_INFO(class_name, key_type_name) 支持任意键类型。

IMPLEMENT_TYPE_INFO 将键值与类绑定。这正是如果必须保存类型时存档将存储到文件中的值。对于所有支持序列化的类,键必须是唯一的。

REGISTER_KEY_SERIALIZATION 将键类型与归档绑定。可以使用不同的键与不同的归档。如果类被序列化,其绑定的键类型必须与归档绑定的键类型相同。否则会抛出异常。

REGISTER_CLASS_SERIALIZATION 将类注册到归档中,以便可以将该类的引用和指针存储到指定的归档中。

更高级的示例

AdvancedExample 展示了序列化更实际的用法。文件 AppVersion.h 包含定义应用程序版本的宏。每个版本都会更改 City.h 中的数据,因此需要迁移。我在那里添加了不同类型的迁移——所有这些都可以在 Load 方法中直接解决。

重要文件

City.h

序列化数据类型的声明

City.cpp

数据类型的实现

metaCity.cpp

数据类型的序列化

Archive.h

City.h 中使用的存档声明

Archive.cpp

档案的实现

AppVersion.h

定义当前应用程序版本(更改 #define

应用程序描述

该应用程序创建4个具有预定义数据值的项目(城市)。根据 AppVersion.h 中的应用程序版本,它创建仅该版本支持的数据。数据可以存储到文件中,也可以选择目标应用程序版本。因此,可以导出数据,将应用程序版本切换到更高版本,然后导入文件。或者从更高版本导出文件到更低版本,然后在更低版本中加载文件。

代码文档

宏描述

DeclareMacros.h 包含应在头文件中使用的宏,而 ImplementMacros.h 包含应在实现文件中使用的宏。

DeclareMacros.h

DECLARE_TYPE_INFO(class_name, key_type_name)

将类型与键类型绑定。必须为 key_type_name 提供 DirectValueReaderDirectValueWriter 模板的特化。

DECLARE_TYPE_INFO_STRING_KEY(class_name)

将类型与 std::string 键类型绑定。

DECLARE_TYPE_INFO_WSTRING_KEY(class_name)

将类型与 std::wstring 键类型绑定。

ImplementMacros.h

IMPLEMENT_TYPE_INFO(类名, 键, 版本号, ...)

DECLARE_TYPE_INFO 宏的实现。

类名 类的名称
键值(在所有 IMPLEMENT_TYPE_INFO 使用中必须是唯一的)
版本号    用于启用加载旧档案。每次类成员集更改时,版本号都应增加。
... 父类列表(仅支持序列化的类)

REGISTER_CLASS_SERIALIZATION(in_archive_name, out_archive_name, class_name)

创建 TypedInArchiveObjectBinderTypedOutArchiveObjectBinder 的特化。这两个模板的特化要求输入类具有以下两个方法

void Save(out_archive_name& ar, const int classVersion) const;
void Load(in_archive_name& ar, const int classVersion);

或作为独立函数

void Save(out_archive_name& ar, const class_name& obj, const int classVersion);
void Load(in_archive_name& ar, class_name& obj, const int classVersion);

REGISTER_KEY_SERIALIZATION(in_archive_name, out_archive_name, key_type_name)

将 key_type_name 与提供的归档绑定。只有使用相同键类型注册的类型才能与归档序列化。

TypedSharedPtrHolder.h

DECLARE_SHARED_PTR0、DECLARE_SHARED_PTR1、DECLARE_SHARED_PTR2

宏用于为类启用共享指针的序列化。数字定义了类型有多少个父类。

class C_no_parents { ... };
DECLARE_TYPE_INFO(C_no_parents);
DECLARE_SHARED_PTR0(C_no_parents, std::shared_ptr<C_no_parents>); 

class C_single_parent : public A { };
DECLARE_TYPE_INFO(C_single_parent );
DECLARE_SHARED_PTR1(C_single_parent, std::shared_ptr<C_single_parent>, std::shared_ptr<A>); 

class C_two_parents : public B, public A { };
DECLARE_TYPE_INFO(C_two_parents);
DECLARE_SHARED_PTR2(C_two_parents, std::shared_ptr<C_two_parents>, std::shared_ptr<B>, std::shared_ptr<A>);

该库仅支持最多两个父类的共享指针。但如果需要,编写 DECLARE_SHARED_PTR3DECLARE_SHARED_PTR4 等宏也不是问题。

自定义存档类型

详情请查看 BinaryInArchiveBinaryOutArchive。重要的一步是继承自提供流运算符的模板 InArchiveOutArchiveInArchive 模板要求主存档类具有一个成员方法

void Read(void* pBuffer, size_t size);

OutArchive 模板需要以下方法

void Write(const void* pBuffer, size_t size);

Read 方法可以由您自己添加,也可以通过将 BinaryInFileComposition 用作另一个父类来添加。Write 方法也可以由您自己添加,或者通过将 BinaryOutFileComposition 用作另一个父类来添加。

辅助模板

整个库通过模板特化来定制用户类型的序列化。这些特化由下面“类描述”部分中描述的归档对象绑定器使用。

不可默认构造的类

如果类不提供默认构造函数,则必须提供 ReadConstructDataImplWriteConstructDataImpl 的特化。

template<typename ArchiveT, typename ObjectT, typename Enabled = void>
struct ReadConstructDataImpl
{
    static void Invoke(ArchiveT& /*ar*/, ObjectT* pMemory, const int /*classVersion*/)
    {
        // read input parameters
        ...
        // call class constructor
        ::new(pMemory) ObjectT(...); // pass input parameters to constructor
    }
};

template<typename ArchiveT, typename ObjectT, typename Enable = void>
struct WriteConstructDataImpl
{
    static void Invoke(ArchiveT& /*ar*/, const ObjectT& /*obj*/, const int /*classVersion*/)
    {
        // write all parameters to be able to call constructor in ReadConstructDataImpl::Invoke call
    }
};

请注意,最后一个模板参数 Enable 可以通过使用 std::enable_if 用于一组类。假设您有一个中间类 A 接受两个字符串,并且您从 A 继承了三个类,它们具有与 A 相同的构造函数签名(以便能够调用父构造函数)。在这种情况下,您可以编写

template<typename ArchiveT, typename ObjectT>
struct WriteConstructDataImpl<ArchiveT, ObjectT, typename std::enable_if<std::is_base_of<A, ObjectT>::value>::type>
{
    static void Invoke(ArchiveT& ar, const ObjectT& obj, const int /*classVersion*/)
    {
        ar << obj.GetString1();
        ar << obj.GetString2();
    }
};

template<typename ArchiveT, typename ObjectT>
struct ReadConstructDataImpl<ArchiveT, ObjectT, typename std::enable_if<std::is_base_of<A, ObjectT>::value>::type>
{
    static void Invoke(ArchiveT& ar, ObjectT* pMemory, const int /*classVersion*/)
    {
        std::string s1, s2;
        ar >> s1 >> s2;
        ::new(pMemory) ObjectT(s1, s2);
    }
};

不使用 DECLARE_TYPE_INFO 的类型序列化

可以在没有 DECLARE_TYPE_INFO 的情况下序列化类型。但是,在这种情况下,序列化库需要 DirectValueWriterDirectValueReader 模板的特化。

struct MyHelperDataType
{
    int a, b, c;
};
template<>
struct DirectValueWriter<MyOutArchive, MyHelperDataType>
{
    static void Invoke(MyOutArchive& ar, const MyHelperDataType& value)
    {
        ar << value.a << value.b << value.c;
    }
};

template<>
struct DirectValueReader<MyInArchive, MyHelperDataType>
{
    static void Invoke(MyInArchive& ar, MyHelperDataType& value)
    {
        ar >> value.a >> value.b >> value.c;
    }
};

缺点是该类型的指针无法序列化到/从归档中。此外,如果成员发生更改,数据迁移也无法轻松执行。

自定义内存分配支持

template<typename ArchiveT, typename ObjectT, typename Enabled = void>
struct AllocateDataImpl
{
    static void* Invoke(ArchiveT& /*ar*/, const int /*classVersion*/)
    {
        return malloc(sizeof(ObjectT));
    }
};
template<typename ArchiveT, typename ObjectT, typename Enabled = void>
struct DeallocateDataImpl
{
    static void Invoke(ArchiveT& /*ar*/, const int /*classVersion*/, void* pMemory)
    {
        free(pMemory);
    }
};

您可以编写这些模板的自己的特化,以提供自己的分配。

如果库需要直接构造一个类型,它会使用模板类 ConstructDefaultValue。这个模板也适用于自定义内存分配支持。例如,如果需要从归档中读取容器的容器,则无法提供内部容器的自定义构造。对 std 容器的支持正是使用 ConstructDefaultValue 模板来构造存储的类型,因此实现能够将自定义分配器传递给这些容器的构造函数。

读写 std::unique_ptr 模板也可以使用一些自定义删除器。该库提供了 WriteUniquePtrDeleterReadUniquePtrDeleter,以允许写入绑定到删除器的自定义数据。

共享指针的支持在单独的章节中讨论。

序列化父类内容

由于类的序列化可以作为成员方法或独立函数实现,因此不清楚如何序列化父类数据。为了统一调用,无论父类序列化实现如何,库都包含一个模板 BaseObject。模板参数是父类型。请注意,如果存储数据,父类型应为 const T

void B::Save(MyOutArchive& ar, const int /*classVersion*/) const
{
    ar << BaseObject<const A>(*this);
}
void B::Load(MyInArchive& ar, const int /*classVersion*/)
{
    ar >> BaseObject<A>(*this);
}

Serialization::Access 和友元访问

如果使用成员方法实现序列化,则如果授予 Serialization::Access 对该类的友元访问权限,则这些方法可以声明为私有。

classs MyClass
{
    friend Serialization::Access;
private:
    void Save(MyOutArchive& ar, const int classVersion) const;
    void Load(MyInArchive& ar, const int classVersion);
};

共享指针支持

必须使用宏 DECLARE_SHARED_PTR0(或 DECLARE_SHARED_PTR1DECLARE_SHARED_PTR2,根据类型具有多少个父类)为类声明共享指针类型。然后还需要提供模板的特化

template<typename ArchiveT, typename SharedPtrT, typename Enabled = void>
struct CreateSharedPtrImpl
{
    static SharedPtrT Invoke(ArchiveT& /*ar*/, SharedPtrT::value_type* /*pMemory*/)
    {
        // wrap a created object to a shared pointer
        ...    
    }
};

除了上述模板特化之外,库还期望以下特化

template<typename T>
struct SharedPtrValueGetter
{
    static void* Invoke(const T& sharedPtr)
    {
        // extract raw pointer from a shared pointer
        ...
    }
};
template<typename WeakPtrT>
struct ToSharedPtr
{
	using SharedPtrT = ...; // shared pointer from WeakPtrT
	static SharedPtrT Invoke(const WeakPtrT& ptr)
	{
		// convert weak pointer to shader pointer
		return ...;
	}
};
template<typename T, typename U>
struct UpCastSharedPtr
{
	U Invoke(const T& /*ptr*/)
	{
		// convert a shared pointer T to a shared point U. It's up to the specialization to verify that types are related and convertible.
		return ...;
	}
};

该库内置了对 std::shared_ptrstd::weak_ptr 的支持(请参阅 StdSharedPtrImpl.h)。

异常处理

错误报告通过异常完成。库抛出的所有异常都继承自 Serialization::SerializationException,它们位于 Exceptions.h 文件中。

STD 容器

该库内置了对 STD 容器序列化的支持。如果代码中需要,必须包含 InArchiveStdFunctors.hOutArchiveStdFunctors.h。也可以为 boost 容器编写序列化,但 boost 容器太多了,我宁愿不写。

模板类的序列化

模板类很棘手。不同的模板参数会产生不同的类型。序列化库需要为库应支持的每种类型提供唯一的键。

template<typename T>
class MyTemplate
{
...
};
DECLARE_TYPE_INFO(MyTemplate, std::string); // <-- DOESN'T WORK !!

在模板的情况下,可以扩展和调整宏 DECLARE_TYPE_INFO 生成的代码

namespace Serialization
{
namespace Detail
{
template<typename T>
struct TypeInfoTraits<MyTemplate<T>> // partial specialization
{
using value_type = MyTemplate<T>;
using key_type = std::string; // or whatever key type is needed
};
}
}

但不能以这种方式欺骗 IMPLEMENT_TYPE_INFO,因为对于每种类型,都必须提供一个键值(如上述示例中的字符串)。因此,每个实例化模板都必须有一个 IMPLEMENT_TYPE_INFO

IMPLEMENT_TYPE_INFO(MyTemplate<int>, "MyTemplateInt", 0);
IMPLEMENT_TYPE_INFO(MyTemplate<char>, "MyTemplateChar", 0);
IMPLEMENT_TYPE_INFO(MyTemplate<bool>, "MyTemplateBool", 0);

由于需要为每个实例化模板编写 IMPLEMENT_TYPE_INFO,我也会为这些类型单独编写 DECLARE_TYPE_INFO,如果序列化放在单独的模块中,则从 DLL 导出实例化模板类型。

存档对象绑定器

ArchiveObjectBinder

将归档和对象类型绑定在一起,以便可以在这两种类型上调用特定方法,而无需使用公共基类。该类作为定义输入和输出归档对象绑定器接口的基类。

InArchiveObjectBinder

从档案读取数据到对象的接口类


void Read(BaseInArchive& ar, void* ptr, const int classVersion) const

读取对象内容。


void ReadConstructData(BaseInArchive& ar, void* ptr, const int classVersion) const

读取用于构造对象的输入数据。


void* AllocateObject(BaseInArchive& ar, const int classVersion) const

分配一个对象(构造函数尚未调用)。


void DeallocateObject(BaseInArchive& ar, const int classVersion, void* pMemory) const

释放之前由 AllocateObject 调用分配的内存。


void DestructObject(BaseInArchive& ar, const int classVersion, void* pMemory) const

调用绑定对象的析构函数。


std::unique_ptrCreateSharedPtr(BaseInArchive& ar, void* ptr) const

将创建并加载的指针包装到共享指针包装器中。在“共享指针支持”部分中描述。


void GetInputObjects(BaseInArchive& ar, void* ptr, LoadedPointerInfoArray& inputObjects) const

支持复杂的对象迁移。在“复杂迁移支持”部分中描述。


bool PostLoad(BaseInArchive& ar, void* ptr, const int classVersion) const

支持复杂的对象迁移。在“复杂迁移支持”部分中描述。

InArchiveKeyBinder

用于从归档读取键的接口。键描述了归档中写入的类类型。重要的归档对象绑定器需要支持写入/读取多态指针。


const TypeInfo::Key& Read(BaseInArchive& ar) const

从档案中读取一个键。

DeferredInArchiveObjectBinder

支持直接读取对象,例如存储在容器中的对象。此归档对象绑定器允许在 std::vector 类似容器中使用不可默认构造的对象。

OutArchiveObjectBinder

用于将数据从对象写入档案的接口类。


void Write(BaseOutArchive& ar, const void* ptr, const int classVersion) const

从输入对象写入内容。


void WriteConstructData(BaseOutArchive& ar, const void* ptr, const int classVersion) const

写入输入对象的构造函数所需的数据。

OutArchiveKeyBinder

用于向归档写入键的接口。该键与类类型绑定在一起,以支持指针的深度写入。


void Write(BaseOutArchive& ar, const TypeInfo::Key& key) const

将键写入档案。

DeferredOutArchiveObjectBinder

支持直接写入对象,这些对象可以通过 DeferredInArchiveObjectBinder 加载。它写入调用写入对象的构造函数所需的所有数据以及对象的内容。

template<typename ArchiveT, typename ObjectT> TypedInArchiveObjectBinder

InArchiveObjectBinder 的主要实现。它将归档和对象的实际类型绑定在一起。实现通过使用更进一步的模板类进行定制,因此无需为特定的归档和类类型重新实现该类,而只需特化子模板。输入 ArchiveT 必须继承自 BaseInArchive,输入 ObjectT 必须是非指针且不能是 const。此类的实例通过使用宏 REGISTER_CLASS_SERIALIZATION 创建。所有方法的输入参数都由 BaseInArchive 确保是正确的类型。


void Read(BaseInArchive& ar, void* ptr, const int classVersion) const

调用 ObjectT::Load 成员方法或独立的 void Load(ArchiveT&, ObjectT&, int) 函数。


void ReadConstructData(BaseInArchive& ar, void* ptr, const int classVersion) const

对于抽象类,它什么也不做,并且不应该调用该方法(如果调用,它会抛出异常)。对于非抽象类,它会调用默认构造函数。它使用模板类 ReadConstructDataImpl 来定制行为。


void* AllocateObject(BaseInArchive& ar, const int classVersion) const

为一个对象分配内存。对于抽象类,它什么也不做,并且不应该调用该方法(如果调用,它会抛出异常)。对于非抽象类,它使用 malloc 分配内存。可以通过特化 AllocateDataImpl 模板来自定义行为。


void DeallocateObject(BaseInArchive& ar, const int classVersion, void* pMemory) const

释放之前由 AllocateObject 调用分配的对象内存。对于抽象类,它什么也不做,并且不应该调用该方法(如果调用,它会抛出异常)。对于非抽象类,它使用 free 释放内存。可以通过特化 DeallocateDataImpl 模板来自定义行为。


void DestructObject(BaseInArchive& ar, const int classVersion, void* pMemory) const

调用之前由 ReadConstructData 方法构造的对象的析构函数。不应在抽象类上调用此方法(它会抛出异常)。原因是 ReadConstructData 不允许在抽象类上调用,所以此方法也不允许。可以通过特化 DestructDataImpl 模板来自定义行为。


std::unique_ptr<SharedPtrWrapper> CreateSharedPtr(BaseInArchive& ar, void* ptr) const

创建共享指针包装器。在“共享指针支持”部分中描述。


void GetInputObjects(BaseInArchive& ar, void* ptr, LoadedPointerInfoArray& inputObjects) const

如果 ArchiveT 启用了复杂迁移,它会调用 ObjectT::GetInputObjects 成员方法或独立的 void GetInputObjects(ArchiveT&, ObjectT&, LoadedPointerInfoArray&) 函数。在“复杂迁移支持”部分中进行了更详细的描述。


bool PostLoad(BaseInArchive& ar, void* ptr, const int classVersion) const

如果 ArchiveT 启用了复杂迁移,它会调用 ObjectT::PostLoad 成员方法或独立的 bool PostLoad(ArchiveT&, ObjectT&, const int) 函数。在“复杂迁移支持”部分中进行了更详细的描述。

template<typename ArchiveT, typename KeyT> TypedInArchiveKeyBinder

InArchiveKeyBinder 的实现,用于读取特定类型的键。此模板的特化由 REGISTER_KEY_SERIALIZATION 宏创建。

template<typename ArchiveT, typename ObjectT> TypedDeferredInArchiveObjectBinder

DeferredInArchiveObjectBinder 的实现。如果对象必须直接从档案加载,则类的实例会直接在堆栈上创建。

ObjectT Read(BaseInArchive& ar);

从归档读取对象。ObjectT 必须是可移动的,但不一定是可复制的。

template<typename ArchiveT, typename ObjectT> TypedOutArchiveObjectBinder

OutArchiveObjectBinder 的主要实现。它将归档和对象的实际类型绑定在一起。实现通过使用更进一步的模板类进行定制,因此无需为特定的归档和类类型重新实现该类,而只需特化子模板。输入 ArchiveT 必须继承自 BaseInArchive,输入 ObjectT 必须是非指针且不能是 const。此类的实例通过使用宏 REGISTER_CLASS_SERIALIZATION 创建。所有方法的输入参数都由 BaseOutArchive 确保是正确的类型。


void Write(BaseOutArchive& ar, const void* ptr, const int classVersion) const

调用 ObjectT::Save 成员方法或独立的 void Save(ArchiveT&, const ObjectT&, int) 函数。


void WriteConstructData(BaseOutArchive& ar, const void* ptr, const int classVersion) const

对于抽象类,它什么也不做,并且不应该调用该方法(如果调用,它会抛出异常)。对于非抽象类,它使用模板类 WriteConstructDataImpl 来定制行为。默认情况下,模板什么也不做,但可以编写特化来存储用于构造类的输入数据。

template<typename ArchiveT, typename KeyT> TypedOutArchiveKeyBinder

OutIArchiveKeyBinder 的实现,用于写入特定类型的键。此模板的特化由 REGISTER_KEY_SERIALIZATION 宏创建。

template<typename ArchiveT, typename ObjectT> TypedDeferredOutArchiveObjectBinder

DeferredOutArchiveObjectBinder 的实现。如果对象必须直接保存到档案中,则类的实例将直接在堆栈上创建。

void Write(BaseOutArchive& ar, const ObjectT& obj)

将对象写入档案。它写入构造数据和对象的内容。

创建与旧版本应用程序兼容的文件

通常,该库不支持此功能。如果应用程序需要导出可与同一应用程序的旧版本加载的文件,我建议完全忽略并不要使用类版本控制。在使用类版本控制系统时,文件版本由文件中序列化的所有类版本集合定义。在这种情况下,需要以某种方式跟踪这些集合,并在其中一个类即将更改时导出类版本。我建议使用一个单一数字来跟踪应用程序文件版本并将其传递给存档。然后,每个 Save/Load 方法都可以访问此数字并仅存储/加载当时的内容。在这种情况下,好的做法是创建一个中间文件版本(尚未发布)。如果类即将更改,则 Save/Load 方法的内容将被复制并保留用于保存/加载旧版本,然后添加 if 语句,并可以调整代码。这与使用类版本控制系统非常相似,但所有类都使用相同的数字。

void Save(MyArchive& ar, const int classVersion) const
{
    if(ar.GetFileVersion() <= APP_FILE_VERSION_1_0)
    {
        // file version 1.0
        ar << m_Data1;
    }
    else if(ar.GetFileVersion() <= APP_FILE_VERSION_2_0)
    {
        // file version 2.0
        ar << m_Data1;
        ar << m_Data2; // new to 2.0 version
    }
    else
    {
        // most recent file version
        ar << m_Data1;
        ar << m_Data2; // new to 2.0 version
        ar << m_Data3; // new to 3.0 version
    }
}

请注意,最后一部分也适用于 4.0、5.0 等版本。如果例如类在 3.0-5.0 版本之间没有更改,但在 6.0 版本中更改,则 Save 方法将如下所示

void Save(MyArchive& ar, const int classVersion) const
{
    if(ar.GetFileVersion() <= APP_FILE_VERSION_1_0)
    {
        // file version 1.0
        ar << m_Data1;
    }
    else if(ar.GetFileVersion() <= APP_FILE_VERSION_2_0)
    {
        // file version 1.1 - 2.0
        ar << m_Data1;
        ar << m_Data2; // new to 2.0 version
    }
    else if(ar.GetFileVersion() <= APP_FILE_VERSION_5_0)
    {
        // file versions 2.1 - 5.0
        ar << m_Data1;
        ar << m_Data2; // new to 2.0 version
        ar << m_Data3; // new to 3.0 version
    }
    else
    {
        // most recent file version
        ar << m_Data1;
        ar << m_Data2; // new to 2.0 version
        ar << m_Data3; // new to 3.0 version
        ar << m_Data4; // new to 6.0 version
    }
}

加载将类似,但在从旧档案加载类时,务必不要忘记初始化新成员

void Load(MyArchive& ar, const int classVersion)
{
    if(ar.GetFileVersion() <= APP_FILE_VERSION_1_0)
    {
        // file version 1.0
        ar >> m_Data1;
        m_Data2 = ...;
        m_Data3 = ...;
        m_Data4 = ...;
    }
    else if(ar.GetFileVersion() <= APP_FILE_VERSION_2_0)
    {
        // file version 1.1 - 2.0
        ar >> m_Data1;
        ar >> m_Data2; // new to 2.0 version
        m_Data3 = ...;
        m_Data4 = ...;
    }
    else if(ar.GetFileVersion() <= APP_FILE_VERSION_5_0)
    {
        // file versions 2.1 - 5.0
        ar >> m_Data1;
        ar >> m_Data2; // new to 2.0 version
        ar >> m_Data3; // new to 3.0 version
        m_Data4 = ...;
    }
    else
    {
        // most recent file version
        ar >> m_Data1;
        ar >> m_Data2; // new to 2.0 version
        ar >> m_Data3; // new to 3.0 version
        ar >> m_Data4; // new to 6.0 version
    }
}

所以如果添加了新成员,就必须将其添加到所有部分。我不建议在这里进行任何优化,比如试图避免重复代码。

void Load(MyArchive& ar, const int classVersion)
{
    ar >> m_Data1;
    if(ar.GetFileVersion() >= APP_FILE_VERSION_2_0)
        ar >> m_Data2; // new to 2.0 version
    else
        m_Data2 = ...;
    if(ar.GetFileVersion() >= APP_FILE_VERSION_5_0)
        ar >> m_Data3; // new to 3.0 version
    else
        m_Data3 = ...;
    if(ar.GetFileVersion() >= APP_FILE_VERSION_6_0)
        ar >> m_Data4; // new to 6.0 version
    else
        m_Data4 = ...;
}

首先,这很混乱,相信我,在此期间迁移可能会变得很糟糕,而且在这种代码中很容易出错。最重要的规则是,如果应用程序已经发布给公众,并且文件应该可以从该发布的应用程序中导出或导入,最好保持代码不变。这就是为什么最好复制它,包装在条件中,并且只更改当前应用程序版本的复制代码。

复杂迁移支持

数据迁移用于类更改时,仍然需要能够加载已经创建(并可能已经交付给客户)的文件。最简单的迁移类型是类直接更改其成员——例如成员的数据类型更改、添加或删除。为了这个简单的目的,库具有类版本控制。如果每次更改都增加类版本,则可以在加载期间验证类版本并相应地调整数据。然而,实际示例更复杂,并非总是可以通过 Load 方法/函数迁移类数据。在应用程序的生命周期中,单个类可以快速增长,然后可能需要将一个类拆分为两种类型。或者相反的更改——将两种类型合并为一种类型。另一种迁移类型可以是基于多个对象的数据计算。在序列化期间,无法定义对象存储/加载的顺序,因此从类的成员指针读取数据可能会导致问题。该库支持在所有对象加载后执行更复杂的迁移。如果需要这种支持,则输入归档(InArchive 的实现)必须在宏 ENABLE_ARCHIVE_MIGRATION 中使用。通过使用此宏,所有使用 REGISTER_CLASS_SERIALIZATION 注册的类都必须声明额外的接口

As stand-alone functions:
    void GetInputObjects(ArchiveT& ar, T& obj, LoadedPointerInfoArray& inputObjects);
    bool PostLoad(ArchiveT& ar, T& obj, const int classVersion);
or member methods:
    void GetInputObjects(ArchiveT& ar, LoadedPointerInfoArray& inputObjects);
    bool PostLoad(ArchiveT& ar, const int classVersion);

迁移应放在 PostLoad 方法/函数中。GetInputObjects 定义了 PostLoad 的调用顺序。让我们考虑以下类

class A
{
    ...
public:
    B* m_pB;
    C* m_pC;
    D* m_pD; // added in classVersion 1
};
void Save(MyOutArchive& ar, const A& a, const int classVersion)
{
    ar << a.m_pB << a.m_pC;
    if(classVersion >= 1)
        ar << a.m_pD;    
}
void Load(MyInArchive& ar, A& a, const int classVersion)
{
    ar >> a.m_pB >> a.m_pC;
    if(classVersion >= 1)
        ar >> a.m_pD;
    else
    {
        // m_pD should be initialize, but how?
        ...
    }
}

撇开糟糕的设计(a.m_pBa.m_pC 在加载前应确保为 nullptr,或者如果类 A 是所有者,则 Load() 函数应释放指针),只需考虑类 A 需要从 BC 指针中提取一些数据,并为 classVersion == 0 初始化 m_pD。在 Load() 中,不能确定 BC 是否已加载——如果 BC 指回 A,并且序列化首先调用存储指向 B 的指针,那么在加载 A 期间,指针 B 只是部分初始化——B 的构造函数已被调用,但 B::Load() 尚未完成,因此并非所有成员都已从归档中加载。对于这种情况,需要 PostLoad。需要告诉输入归档,我们希望在 BC 初始化后接收 A::PostLoadGetInputObjects() 正是用于此目的的函数

void GetInputObjects(MyInArchive& ar, A& a, Serialization::LoadedPointerInfoArray& inputObjects)
{
	Serialization::AddInputObject(ar, inputObjects, *m_pB);
	Serialization::AddInputObject(ar, inputObjects, *m_pC);
}

LoadedPointerInfoArray 的填充不能直接进行,库而是提供了辅助模板函数 AddInputObject 来创建一个项,然后将其存储到数组中。这确保了 PostLoad 首先在 BC 上调用(除非 BC 通过其 GetInputObjects 指定另一个类作为其输入,否则顺序未定义),然后才在 A 上调用。因此,APostLoad 实现可以正确初始化 m_pD 成员指针。当调用 MigrationManager::Execute 时,会触发通知。如果类是从其他类继承的,在 PostLoadGetInputObjects 方法中,也需要调用父方法。同样,与 Load/Save 类似,父方法/函数应通过 BaseObject 模板调用。库的 InArchiveMigration 具有接受 BaseObject 引用并调用相应通知的成员方法。

如果需要执行更深层次的、涉及更多不直接相关的对象的层次迁移——例如,迁移一个完整的对象数组,MigrationManager 提供了数据包和迁移器(migrator)的概念。通过这种支持,所有类都应该能够访问 MigrationManager。该库提供了一个模板 InArchiveMigration,它具有成员方法 GetMigrationManager() 来提供此访问。只需将自定义存档的基类从 InArchive 更改为 InArchiveMigration

数据包是只包含数据的小类。在序列化期间,LoadPostLoad 函数可以收集对象和其他数据并将其存储到数据包中。稍后当迁移器执行时,可以访问这些数据包并处理其中的数据。数据包由 MigrationManagerRegisterPacketUnregisterPacketGetPacket 方法管理。该库包含一个辅助模板类 Serialization::PacketImpl,它将数据包与指定的键类型绑定。与可序列化类相反,具有不同键类型的数据包可以注册到 MigrationManager(因为它们从不存储到归档中,所以它不起主要作用)。实现带有静态 getter 的数据包以从归档中提取数据包是很方便的

class MyDataPacket : public Serialization::PacketImpl<std::string>
{
public:
    MyDataPacket()
    : Serialization::PacketImpl<std::string>("MyDataPacket_key")
    {
    }

    MyDataPacket& Get(MyArchive& ar)
    {
        MyDataPacket ref; // just to extract a key
        auto* pPacketRawPtr = static_cast<MyDataPacket*>(ar.GetMigrationManager().GetPacket(ref.GetKey()));
        if(pPacketRawPtr == nullptr)
        {
            auto pPacketPtr = std::make_unique<MyDataPacket>();
            pPacketRawPtr = pPacketPtr.get();
            if(!ar.GetMigrationManager().RegisterPacket(std::move(pPacketPtr)))
            {
                // or whatever exception you prefer
                throw std::runtime_error("Packet was not registered!!");
            }
        }
        return *pPacketRawPtr;
}
private:
    ... // data members
};

缺点是每个数据包都必须有一个类型,所以相同类型的数据包不能注册两次。另一方面,重用数据包需要维护某种键列表。

迁移器是用于进行复杂迁移的类。它是整个文档加载并以 PostLoad 方法迁移/初始化之后的最后一个迁移阶段。迁移器也在 Load/PostLoad 调用期间注册,并且通常与创建数据包一起注册。无法定义迁移器的调用顺序,也不建议在那里创建依赖关系。如果存在依赖关系,最好将两个或更多迁移器合并在一起,并在单个迁移器中解决依赖关系。

MigrationManager 描述

bool Execute(BaseInArchive& ar)

在存档中所有已加载对象(指针)上调用 GetInputObjects,构建依赖图,并按请求顺序调用 PostLoad 通知。

template<typename T> void AddExternObject(BaseInArchive& ar, T& inputObject)

将外部指针注册到档案。在加载期间,档案不会存储已加载引用的地址,因为这些地址可能会被其他已加载对象使无效。只需考虑存储在 std::vector 中的对象。如果插入新对象,存储空间会重新分配。档案无法跟踪此类更改。如果用户希望对这些对象调用通知,则由用户将这些对象注册到 MigrationManager。请注意,即使作为引用从档案加载,所有通过 GetInputObjects 添加的对象也将收到 PostLoad 通知。因此,AddExternObject 主要应与不是任何其他对象的输入对象的顶层引用一起调用。

void AddMigrator(MigratorPtr pMigrator)

向 MigrationManager 注册一个迁移器。类型必须是唯一的。尝试注册两个相同类型的迁移器将失败。

Migrator* FindMigrator(const type_info& migratorInfo) const

按类型查找迁移器。如果未找到,则返回 nullptr。

bool RegisterPacket(PacketPtr pPacket)

向 MigrationManager 注册一个数据包。如果数据包持有的键已被另一个数据包使用,则注册失败。

void UnregisterPacket(const TypeInfo::Key& key)

注销已注册的数据包。如果密钥未被任何数据包使用,则该方法不执行任何操作。

Packet* GetPacket(const TypeInfo::Key& key) const

通过键查找数据包。如果未找到数据包,则返回 nullptr。

编译错误,模板混乱等

最初我想拥有一个易于集成的库。对我来说,易于集成意味着如果在集成过程中犯了错误,很容易解决编译错误。我认为我在这里失败了,编译错误很奇怪,就像集成 boost 序列化库时出现的那些错误一样。主要问题在于模板本身。如果一个模板被特化,它就完全是一个新类,程序员可以随心所欲地编写任何东西。当然,库希望这些类具有一些特性,例如具有特定接口的静态函数,但我没有找到验证类接口是否正确的方法。所以编译器会产生关于调用的奇怪错误。其他类型的错误来自模板的可见性。如果库期望模板的特化,您的特化必须在此处可见。我在代码中添加了尽可能多的 static_asserts,以明确哪里出了问题,但这并非总是可能的。

实施说明

该库仅在 ObjectDependencyGraph.cpp 中使用 boost 进行依赖排序(使用 boost 图库)。单元测试也是使用 boost(boost 框架库)创建的,但它们不需要用于构建库本身。

历史

2016年6月21日 首次发布

2016年6月27日 添加了 PointerOwnershipTests 并修复了加载两个相同类型的空指针作为 std::unique_ptr 时的错误

© . All rights reserved.