实现一个 Variant

实现一个 Variant

http://www.cnblogs.com/catch/p/4903653.html

很多时候我们希望能够用一个变量来保存和操作不同类型的数据(比如解析文本创建 AST 时保存不同类型的结点),这种需求可以通过继承来满足,但继承意味着得使用指针或引用,除了麻烦和可能引起的效率问题,该做法最大的不便还在语义上,指针和引用都不是值类型。于是我们想到 union,union 对简单类型来说是很好的解决思路,它的出现本身也是为了解决这个问题,只是它到底是 C 语言世界里的东西,在 C++ 里面它没法很好的支持用户自定义的类型,主要原因是它不能方便和智能地调用自定义类型的构造和析构函数,即使是到了 c++11 也没法很好解决。

所以,如果我们能设计出这样一种类似 union 的东西,它继承了 union 的所有优点,并且还可以类型安全(因此可以存放任意类型的值,当然前提是可以 copyable & movable),从而不用担心构造和析构的问题,那世界将会变得多么美好。。。这个美好的世界其实已经存在了,它就是 boost 里的 Variant,出于对它实现的好奇,我找到了 Andrei Alexandrescu 的这篇文章,推荐读者们也读一读。

当然只说不练是不够的,Andrei 的实现是基于年代久远的 c++ 98/03,很多东西实现起来很不方便,而现在我们有了 c++11,到了可以用新武器来解决旧问题的时候了(正好标准库里又没这个东西)。

使用场景

我的实现希望能全面模仿 boost 里的 Variant,因此它的使用要求其实非常的简单:

  1. 可以支持任意数量的类型,并且能像简单类型一样对其赋值,而且值是不同的类型。
  2. 通过 variant::get<type>() 这样的方式来获取保存在里面的值。
  3. 除此,还需要支持获取指针(从而类型错误时不用抛异常),以及支持 emplace_set()(类似 vector 里的 emplace_back()).
  4. 支持 copy 和 move 语义。

总结起来,就是要能满足如下一些简单的使用用例:

// 构造
Variant<int, double, string> v1(32);
Variant<int, double, string> v2 = string("www");
Variant<int, double, string> v3(v2);

int k = v1.GetRef<int>();
assert(k == 32);

string& s = v2.GetRef<string>();
assert(s == "www");
assert(v3.GetRef<string>() == "www");

// 赋值
v1 = 23;
assert(v1.GetRef<int>() == 23);
v1 = "eee";
assert(v1.GetRef<string>() == "eee");

v1.emplace_set<string>(4, 'a'); 
assert(v1.GetRef<string>() == "aaaa");

// 拷贝
v1 = v2;
assert(v1.GetRef<string>() == "www");
assert(v2.GetRef<string>() == "www");

// move
v2 = std::move(v1);
assert(v2.GetRef<string>() == "www");
assert(v1.Get<string>() == nullptr);
Variant<int, double, string> v4(std::move(v2));
assert(v4.GetRef<string>() == "www");
assert(v2.Get<string>() == nullptr);

支持任意数量的类型

在模板中支持任意数量的类型曾经是个很麻烦的问题,但到了 c++11,变长参数模板(variadic template)的出现直接解决了这个问题,good bye typelist。除此还剩几个问题待解决。

内存与对齐

因为 Variant 中各类型的大小通常不一样,对齐也不一样,怎么用同一块内存来保存这些不同类型的值呢?最直接最省事的想法是 Variant 内部还是用一个 union 作为存储,但是因为要支持任意数量的模板参数,这个方法变得不可行:编译时虽可以获得全部的模板参数,但怎么在 union 中定义各个类型的变量呢?这里宏都不一定有用,变长参数的逐个展开必须用到递归,也许用继承可以把各个类型的变量嵌入到继承的体系中,总之我没想出来具体的解法。Andrei 的做法是划出一块足够大的公共内存然后使用 placement new.

    template <typename ...TS> struct TypeMaxSize;

    template <>
    struct TypeMaxSize<>
    {
        static constexpr std::size_t value = 0;
    };

    template <typename T, typename ...TS>
    struct TypeMaxSize<T, TS...>
    {
        static constexpr std::size_t cur = alignof(T);
        static constexpr std::size_t next = TypeMaxSize<TS...>::value;
        static constexpr std::size_t value = cur > next? cur : next;
    };

   template<class ...TS>
   struct variant_t 
   {
     private:
        constexpr static size_t Alignment() { return TypeMaxSize<TS...>::value; }

     private:
        alignas(Alignment()) unsigned char data_[Alignment()];
   };

如上,TypeMaxSize 这个结构体用来计算模板参数的 alignment 的最大值,参数的展开是常规的递归,值得注意的是 alignof 和 alignas 这两个新关键字,前者用来获取类型 alignment 的大小,后者用于按指定的值来对齐它所修饰的变量,至此,Andrei 论文里提到的处理 alignment 的各式复杂的 trick 就完全用不上了。

标记类型

类型的设置是在编译时完成的,但 Variant 支持在运行时切换不同类型的值,因此我们需要设置一种方式来动态的标记当前保存的是哪种类型的数据,从而可以析构当前值,再保存新的值。Andrei 用 typeid() 来作为类型的 tag,这样的好处之一是模板的参数顺序就变得不重要了,甚至类型重复也影响不大,但我觉得 Variant 的定义应该严格一些,比如, Variant<int, double> 就不能写成 Variant<double, int>(毕竟本来这两种写法就表示不同的类型了),类型的顺序要固定,因此实际上我们可以利用类型在模板参数列表中的位置作为该类型在 Variant 中的 id,这样做的好处是非常直观简单。如下代码用来检查某个类型是否存在于模板的变长参数列表中,如果存在,顺便计算它的位置(从 1 开始),注意,这些都是编译时的计算。

    // check if a type exists in the variadic type list
    template <typename T, typename ...TS> struct TypeExist;

    template <typename T>
    struct TypeExist<T>
    {
        enum { exist = 0 };
        static constexpr std::size_t id = 0;
    };

    template <typename T, typename T2, typename ...TS>
    struct TypeExist<T, T2, TS...>
    {
        enum { exist = std::is_same<T, T2>::value || TypeExist<T, TS...>::exist };
        static constexpr std::size_t id = std::is_same<T, T2>::value? 1 : 1 + TypeExist<T, TS...>::id;
    };

有了上面的代码,我们可以尝试写一下 Variant 的构造函数:

   template<class ...TS>
   struct variant_t 
   {
     template<class T>
     variant_t(T&& v): type_(TypeExist<T, TS...>::id
     {
        static_assert(TypeExist<T,TS...>::exist, "invalid type for Variant.");
        // placement new to construct an object of T.
        new(data_)(std::forward<T>(v));
     }

     private:
        constexpr static size_t Alignment() { return TypeMaxSize<TS...>::value; }

     private:
        size_t type_ = 0;
        alignas(Alignment()) unsigned char data_[Alignment()];
   };

很简洁,构造函数是个模板,从而可以接受不同类型的值,并就地构造,那么怎么销毁呢?构造时我们知道类型,但析构时,我们却只有一个整型的数字,不知道相对应的类型,因此我们需要一种特殊的反射。

动态选择相应类型的析构函数拷贝函数

虽然在迫切需要类型时,我们只有类型的编号,但这个编号是和类型一一对应的,而针对每个类型的析构函数的调用方式其实是一样的(毕竟析构函数的签名都是一样的),比如,对于任意类型 T, 手动调用它的析构函数,肯定是写成这样:reinterpret_cast<T*>(obj)->~T();,这不赤裸裸暗示我们可以把析构对象的过程写成一个模板函数吗,而且当前 Variant 所需要处理的类型在模板实例化的时候就已经确定了,我们显然可以在实例化模板时,就把各个类型对应的析构函数给实例化一下。

template<class T>
void destroy(unsigned char* data)
{
  reinterpret_cast<T*>(obj)->~T();
}

现在的问题是何时何地去实例化和调用上面的模板函数呢? 显然,模板函数的实例化是肯定要在编译时完成的,因此要在合适的时候把 Variant 的变长参数展开传给 template<class T> void destroy,这不难,但怎么把类型的编号和这些相应的函数对应起来呢?有两种方式,一种是运行时根据类型的 id 来搜索:

template<class ...TS>
struct call
{
  static void call_(size_t, unsigned char*)
  {
     assert(0);
  }
};

template<class T, class ...TS>
struct call<T, TS...>
{
   static void call_(size_t k, unsigned char* data)
   {
      if (k == 0) return;

      if (k == 1) return destroy<T>(data);
      
      call<TS...>::call_(k-1, data);
   }
};

注意上面的代码是怎么把类型列表的展开和具体类型的 id 对应起来的,混合了编译时与运行时的代码,可能不是那么直观明了,但它是能正确工作的,只是它的问题也明显: 引入了没必要的运行时开销。那么,怎么改进呢?一个非常直接的想法是把各个类型对应的 destroy<> 函数在编译时放到一个数组里,运行时只需要根据类型 id 取出相应的函数即可。那么现在的问题变成了,我们能在编译时设置一个数组吗?答案是可以的,而且相当简单。

   template<class ...TS>
   struct variant_t 
   {
     // other definition.
     private:
       using destroy_func_t = void(*)(unsigned char*);

       // 只是声明,需在结构体外再定义。
       constexpr static destroy_func_t fun[] = {destroy<TS>...};
   };

   // 定义 constexpr 数组。
   template<class ...TS>
   constexpr variant_t<TS...>::destroy_func_t variant_t<TS...>::fun[];

编译时的数组其实在 c++11 以前也是支持的,只是再加上支持变长类型的话,写起来比较麻烦罢了。有了如上定义的一个数组,在运行时,我们只根据一个类型 id,就能直接调用相应的析构函数了。

   template<class ...TS>
   struct variant_t 
   {
      // other definition....
     ~variant_t()
      {
        Release();
      }

     // other definition....
     private:
      void Release()
      {
        if (type_ == 0) return;

        fun_[type_ - 1](data_);
      }

     private:
      size_t type_ = 0;
      using destroy_func_t = void(*)(unsigned char*);

      // 只是声明,需在结构体外再定义。
      constexpr static destroy_func_t fun_[] = {destroy<TS>...};

      alignas(Alignment()) unsigned char data_[Alignment()];
   };
   // other definition....

根据类型的 id 来调用相应的拷贝构造函数与 move 构造函数也是同样的做法,这里就不重复了。

拷贝构造和 Move Semantic

经过前面的介绍,一个具备基本功能的 Variant 已经差不多完成了,但我们还没有定义 Variant 本身的 copy 和 move 语义,这个两个功能事关易用性与性能,其实是非常关键的,当然了,实现起来其实就是四个函数:

   template<class ...TS>
   struct variant_t 
   {
      variant_t(variant<TS...>&& v);
      variant_t(const variant_t<TS...>& v);
      variant_t& operator=(variant_t<TS...>&& v);
      variant_t& operator=(const variant_t<TS...>& v);
   }

后面两赋值操作符重载与前面两个构造函数实现上大同小异,这儿只说一说前两个怎么实现。首先注意到,我们前面已经定义了一个模板构造函数用来接受不同类型的值,现在再定义参数类型为 variant_t 的构造函数会和它冲突,因此我们必须想办法使得前面的模板构造函数不能用 variant_t 作为参数实例化,嗯,这显然就得依赖 SFINAE 了。

   template<class ...TS, class D = typename std::enable_if<
            !std::is_same<typename std::remove_reference<T>::type, Variant<TS...>>::value>::type>
   struct variant_t 
   {
     template<class T>
     variant_t(T&& v): type_(TypeExist<T, TS...>::id
     {
        static_assert(TypeExist<T,TS...>::exist, "invalid type for Variant.");

        // placement new to construct an object of T.
        new(data_)(std::forward<T>(v));
     }
     
     // other definition....
   };

这样一来模板构造函数就有两个模板参数了,但是实际上这对用户并没有影响,因为构造函数的模板参数是没法由用户显式去指定的(因为构造函数没法直接调用),它们只能由编译器推导,而这里第二个参数是由我们自己定义的,因此用户也完全没办法影响它的推导,当然了,问题还是有的,接口变得有些吓人了,虽然本质没变。有了如上定义,我们就可以顺利地写出如下代码:

   template<class ...TS, class D = typename std::enable_if<
            !std::is_same<typename std::remove_reference<T>::type, Variant<TS...>>::value>::type>
   struct variant_t 
   {   
     // other definition....
     variant_t(variant_t<TS...>&& other)
     {
        // TODO, check if other is movable.
        if (other.type_ == 0) return;

        type_ = other.type_;
        move_[type_ - 1](other.data_, data_);
     }

     variant_t(const variant_t<TS...>& other)
     {
        // TODO, check if other is copyable.
        if (other.type_ == 0) return;

        type_ = other.type_;
        copy_[type_ - 1](other.data_, data_);
     }
   };

上面的 move_ 与 copy_ 都是函数指针数组,和前面讲的各个类型的析构函数数组一样,都是在编译时建立的,只通过类型的 id 就能获取该类型对应的处理函数,非常方便高效。

剩下的问题

至此,一个简单的 Variant 就算完成了,基本的功能都差不多具备,完整的代码读者有兴趣的话可以参看这里,相应的单元测试在,除此还剩下一些比较麻烦的工作没完成,首先是隐式构造,现在的构造函数接受的参数的类型必须是模板参数列表中之一,否则会报错,因此Variant<string, int> v("www")会编译不过,必须改成 Variant<string, int> v(string("www"));。隐式构造虽然看起来功能简单,但是做起来却很麻烦,主要的问题是怎么判断用户想构造哪种类型的值呢?因此需要在实现上一个类型一个类型地去检查,因此复杂麻烦。另外一个做得不是很好的问题是类型检查,现在拷贝构造,赋值构造,move 构造对类型检查不是很严格,如果对应的类型不支持 copy 或 move 的话,出错信息比较难看。最后一个也算是比较大的问题是,现在的实现要求 Variant 所能保存的值必须是 copyable & moveable,哪怕用户从始至终都没有用到其中的 copy 或 move,特别是 copy, 其实使用的场景非常少,大部分情况下 move 就够了,因此实现上最好能像 vector 一样,基本功能只要求 movable,copyable 不应该强制。

参考:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值