抠腚爱揉曼 Coding Iron Man

28Dec/113

虚跟模板可以兼得?!

由某个案例引发出来...

为什么要虚

在OOP的开发过程中,往往会有大量对象的定义和使用,很多时候我们需要将这些对象持久化的保持在外部文件里面,然后再在需要的时候将其读取进来。因为对象的类型数量都很多,一个一个去实现如何写入和读取就特别冗余且笨重。常见的解决方式便是序(反序)列化,简而言之就是为对象的基类都加上serialize/deserialize的虚方法,然后子类只需要实现如何组织需要写入和读取的数据就可以了。此后我们只要有一个serializable的对象便能对其进行读写了。

为什么要模板

但是关于具体的序列化的时候,便会遇到一些跟平台相关的问题,比如不能确定这个对象最后写入到的是一个本地文件、网络流还是JSON这样的中间载体的时候该怎么办?因此我们提供给序列化接口的序列化器这时变得不那么确定了。此外我们也不希望对象的序列化接口明确的知道序列化器是什么,因为这样会让其产生依赖使得对象的使用不独立。综上,我们希望一种模板化的序列化器,只有到对象用到它的时候才会被具体的展开使用。

虚跟模板可以兼得?!

所以我们需要给对象的基类定义一个方法看上去会是:

template<typename Serializer>
virual void Serialize(Serializer& serializer);

但是遗憾的告诉你这是不可能的。为什么?
首先抛开C++的标准不说,单从编译器的实现上面就能否定这一点。虚函数具体是通过虚函数表来表现其多态性的,这个虚函数表实际上是一个函数地址的数组,在建立这个虚函数表的时候所有的虚函数都需要被实现了,这样才能找到有效的函数地址。但另一方面模板方法本身是并不存在的方法,只有在被用到的时候才被具体展开并实例化。因此模板方法的“不确定”性与虚函数的“必须确定”性在理论上便已经矛盾冲突了。

解决方案

首先,为了模板化,不得不先将virtual去掉,这样要求所有对象里面的序列号方法被重新声明为:

template<typename Serializer>
void Serialize(Serializer& serializer);

但是作为序列化方法的调用者,它所能拿到的是所有对象的基类的指针或者引用,它也不能直接调用到子类里面的具体方法。为此目前我能想出的办法就是向下转型,例如:

void Serialize(Base& base)
{
    FooSeraizlier fooSerializer;
    switch(base.GetClassId())
    {
    case DeviedA::ClassID:
        (static_cast<DeviedA*>(&base))->Serialize(fooSerializer);
        break;
    case DeviedB::ClassID:
        (static_cast<DeviedB*>(&base))->Serialize(fooSerializer);
        break;
    case DeviedC::ClassID:
        (static_cast<DeviedC*>(&base))->Serialize(fooSerializer);
        break;
    ......
    }
}

无奈的是类似这样switch case的向下转型必须要手写的,因此当类型过多的时候肯定是很悲剧的,但是如果你有类似python一类的脚本工具来帮你自动生成代码的话,那就放心的使用吧。

Posted by Jay

Tagged as: Leave a comment
Comments (3) Trackbacks (0)
  1. 感觉你这方向不对头,virtual+template了还要出switch case,这就是坏味道。核心矛盾有两个:
    1、纯粹用虚,这要求所有被序列化的对象继承自同一个根,很多情况下满足不了。
    2、纯粹用模板也不行,你已经说了。

    为了避免switch case的写法,你可以用两层设计。首先Serialize接口还是virtual的。
    class Serializable {public: virtual void Serialize(Serializer& serializer) = 0; }

    然后template继承自这个接口,省去一大部分代码重复书写:
    template
    class SerializableWrapper {
    private:
    T& _ref;
    public:
    SerializableWrapper(T& ref):_ref(ref){}
    virtual void Serialize(Serializer& serializer) { _ref.Serialize(serializer); }
    }

    所有类型都可以这样搞。

    如果有需要特别处理的,别犹豫,用模板特化。

    • 目前我的案例的情况是:
      1. 类C以及其各种子类是由模块A定义使用的。
      2. 现在需要在模块B中将类C以及各种子类通过模块B特有的序列化器输出。
      3. 模块A不能依赖模块B,但是模块B可以依赖模块A,因此不能修改模块A里面的类C以及其各种子类让其对模块B产生依赖。

      因为在模块B里面接收到的已经只是类C的基类了,所以不能在模块B里面去展开模板类SerializableWrapper,同样因为上面的原因也不能在模块A里面去添加特有的模板实例化的代码。这个问题就很类似Abstract Factory,因为其在模块B里面已经是“动态”的类型检测了,相对Map映射来说,switch case还是要高效一些吧。

  2. 诡异啊,格式直接乱了。
    template <T> class SerializableWrapper : public Serializable {… }


Leave a comment

(required)

Trackbacks are disabled.