实现一个 Variant

不少时候咱们但愿可以用一个变量来保存和操做不一样类型的数据(好比解析文本建立 AST 时保存不一样类型的结点),这种需求能够经过继承来知足,但继承意味着得使用指针或引用,除了麻烦和可能引发的效率问题,该作法最大的不便还在语义上,指针和引用都不是值类型。因而咱们想到 union,union 对简单类型来讲是很好的解决思路,它的出现自己也是为了解决这个问题,只是它究竟是 C 语言世界里的东西,在 C++ 里面它无法很好的支持用户自定义的类型,主要缘由是它不能方便和智能地调用自定义类型的构造和析构函数,即便是到了 c++11 也无法很好解决。html

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

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

使用场景

个人实现但愿能全面模仿 boost 里的 Variant,所以它的使用要求其实很是的简单:github

  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);
Variant<int, string> v4("abc");

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

string& s = v2.GetRef<string>();
assert(s == "www");
assert(v3.GetRef<string>() == "www");
assert(2, v4.GetType());
assert(v4.GetRef<string>() == "abc");

// 赋值
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> v5(std::move(v2));
assert(v5.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;
        static constexpr std::size_t align = 0;
    };

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

        static constexpr std::size_t cur_align = alignof(T);
        static constexpr std::size_t next_align = TypeMaxSize<TS...>::value;
        static constexpr std::size_t align = cur_align > next_align? cur_align : next_align;
    };

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

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

如上,TypeMaxSize 这个结构体用来在各种型的 size/alignment 中分别找出最大的两个,参数的展开是常规的递归,值得注意的是 alignofalignas 这两个新关键字,前者用来获取类型 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_) typename std::remove_reference<T>::type(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*>(data)->~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;

        destroy_[type_ - 1](data_);
      }

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

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

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

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

隐式构造与类型转换[10.29 更新]

模板构造函数使得咱们能够支持用户使用任意类型的值来构造一个 Variant, 但显然咱们并不须要支持任意类型,也作不到支持任意类型,事实上咱们须要支持的只是两类:

  1. Variant 模板参数中指定的类型。
  2. 可以隐式转换为 Variant 模板参数中的类型的类型,具体来讲,就是要使得 Variant<int, string> v("abc"); 是合法的。

其中第一种类型的参数咱们已经支持了,如今得处理的是第二种类型,因此咱们须要一个能转换类型的东西,它能根据构造函数的模板参数 T,从 Variant 的模板参数列表中选择一个类型 CT,使得 T 能隐式地转换为 CT.

template<class T, class ...TS>
    struct SelectType
    {
       using type = typename std::conditional<TypeExist<T, TS...>::exist, T,
               typename SelectConvertible<T, TS...>::type>::type;
    };

参看如上所示 template<> SelectType,第一步是判断 T 是否已经存在于类型参数列表中了,若是是则直接使用 T,不然的话,咱们就要遍历 TS,从中找出一个类型 CT, 使得 T 能隐式地转换为 CT,判断一个类型是否能隐式地转换为另外一种类型须要一些特别的技巧,比较常见的作法是 Andrei 在 Modern c++ deisgn 里介绍的那种经过函数重载,并判断返回类型来实现类型的选择。

template<class S, class D>
struct is_convertible
{
   struct big { char d[2]; };
   typedef char small;

   static S get_src_type();

   static big foo(D);
   static small foo(...);

   enum { value = sizeof(foo(get_src_type())) == sizeof(big) };
};

判断一个类型是否可转换为另外一个类型实在是太常见了,所以 c++11 里内置了一个功能相同的结构:std::is_convertible<>,正好帮我省一些代码,剩下要作的就只是遍历变长参数列表了。

template<class T, class ...TS>
    struct SelectConvertible
    {
        enum { exist = false };
        using type = void;
    };

    template<class T, class T1, class ...TS>
    struct SelectConvertible<T, T1, TS...>
    {
        enum { exist = std::is_convertible<T, T1>::value || SelectConvertible<T, TS...>::exist };

        using type = typename std::conditional<std::is_convertible<T, T1>::value,
                T1, typename SelectConvertible<T, TS...>::type>::type ;
    };

拷贝构造和 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 的构造函数会和它冲突(当参数是非 const 的左传引用),所以咱们必须想办法使得前面的模板构造函数不接受 variant_t<> 这种类型做为模板参数,嗯,这显然就得依赖 SFINAE 了。

template<class ...TS>
   struct variant_t 
   {
     template<class T, class D = typename std::enable_if<
            !std::is_same<typename std::remove_reference<T>::type, Variant<TS...>>::value>::type>
     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_) typename std::remove_reference<T>::type(std::forward<T>(v));
     }
     
     // other definition....
   };

这样一来模板构造函数就有两个模板参数了,可是实际上这对使用者并无影响,由于构造函数的模板参数是无法由用户显式去指定的(由于构造函数无法直接调用),它们只能由编译器推导,而这里第二个参数是由咱们本身定义的,所以用户也彻底没办法影响它的推导,固然了,问题仍是有的,接口变得有些吓人了,虽然本质没变。有了如上定义,咱们就能够顺利地写出以下代码:

template<class ...TS>
   struct variant_t 
   {   
     // other definition....
     variant_t(variant_t<TS...>&& other)
     {
        // TODO, check if other is movable.
        if (other.type_ == 0) return;

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

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

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

上面的 move_ 与 copy_ 都是函数指针数组,和前面介绍的各种型的析构函数数组同样,都是在编译时创建的,只经过类型的 id 就能获取该类型对应的处理函数,很是方便高效。对于拷贝赋值(copy assignment)与移动赋值(move assignment),实现上相似,但有些细节须要考虑:

  1. 当前 variant 保存的对象的类型与参数 variant 保存的类型同样时,须要执行的操做是 copy assignment 及 move assignment.
  2. 当前 variant 保存的对象的类型与参数 variant 保存的类型不一样时,须要先析构当前保存的对象,而后再 copy/move construct.
  3. 若是 copy/move 抛出了异常,须要确保当前 variant 仍处于一个合法的状态:空或者保持原来的值。不一样的选择只是实现上的取舍,前者好实现些,后者则比较麻烦。

完整的代码请参看这里

优化 copy 和 move 的实现[10.29 更新]

前面提到,copy_ 和 move_ 的实现能够彻底照搬 destroy_,但那样作会引入一个可大可小的问题,咱们强制实例化了 Variant 模板参数列表中每个类型所对应的 copy 和 move 函数,这就使得用户在使用 Variant 时,必须保证其所使用的所有类型都是 copyable 和 movable,这个要求能够说是很严格的,所以很大程度限制了 Variant 的使用范围,那么咱们是否能够优化一下呢?使得 Variant 能像 vector 同样,只对能够 move 和能够 copy 的类型定义那些相应的 copy 函数和 move 函数,而不是一概死板地要求所有类型都必须 movable 和 copyable?答案显然是可行的。

为实现这个功能,咱们须要增长一些辅助性的结构,首先是怎么判断一个类型是否可 copy 或可 move,这能够经过检查该类型是否认义了 copy constructor 和 move constructor 来达到这个目的,具体作法参考 modern c++ design,这里我使用了 c++11 自带的 std::is_copy_constructiblestd::is_move_constructible,而后咱们还须要定义一个模板的 copy/move 函数,并对这些函数进行一个特化,这个特化是专门给不能 copy/move 的类型用的,当用户企图 copy/move 一个不能 copy/move 的类型时,就调用这个特化的函数。

template<class T>
    void CopyConstruct(const unsigned char* f, unsigned char* t)
    {
        new(t) T(*reinterpret_cast<const T*>(f));
    }

    template<>
    void CopyConstruct<void>(const unsigned char*, unsigned char*)
    {
        throw "try to copy Variant object containing non-copyable type.";
    }

接下来就和以前处理 destroy 函数同样,得把它们填充到函数数组里了,由于须要特殊处理那些不能 copy/move 的类型,这里须要借助 std::conditional 来转换一下类型从而选择合适的 copy/move 函数。

constexpr static VariantHelper::copy_func_t copy_[] = {CopyConstruct<typename std::conditional<std::is_copy_constructible<TS>::value, TS, void>::type>...};

如上所示,咱们终于把 copy_ 和 move_ 从新定义好了,其中特化的 CopyConstruct<void> 什么也没作只是抛了一个异常。至此,彷佛该作的功能都差很少完成了,但等等,咱们还有些手尾要处理:虽然咱们再也不实例化那些不能 copy 或不能 move 的类型的 copy 函数和 move 函数,转而在数组里填了一个什么事也没作只会抛异常的空函数,但咱们并没阻止用户去作错误的事情,用户仍是能够把一个不能 copy/move construct 的对象用传左值引用的方式去构造一个 Variant。

NonCopyable nc;

// 如下能够经过编译,但在运行时会抛异常。
variant_t<int, NonCopyable> v(nc);

这显然还不够友好,事实上咱们能够对 Variant 的拷贝构造函数在编译时进行检查,若是发现用户以左值引用的方式传入一个不支持 copy 的参数就报个错,对 move 同理。注意到 Variant 的构造函数是转发类型的模板函数(template<class T> variant_t(T&&)),它既能接受左值引用,也能接受右值引用,所以咱们须要定义一个简单的结构来判断当前的参数是 lvalue reference 仍是 rvalue reference,并对不一样类型的引用进行检查。

template<bool lvalue, class T>
    struct CheckConstructible
    {
        enum { value = std::is_copy_constructible<T>::value };
    };

    template<class T>
    struct CheckConstructible<false, T>
    {
        enum { value = std::is_move_constructible<T>::value };
    };

判断一个类型是左值引用仍是右值引用可使用 std::is_lvalue_reference<>, std::is_rvalue_reference<>,因而咱们能够在 Variant 的构造函数里再加一个 static_assert<>。

template <typename T, typename ...other...>
    variant_t(T&& v)
    {
        static_assert(VariantHelper::TypeExist<T, TS...>::exist,
                     "invalid type for invariant.");

        static_assert(VariantHelper::CheckConstructible<std::is_lvalue_reference<T>::value, T>::value,
                     "try to copy or move an object that is not copyable or moveable.");

        // 其它的代码省略
    }

好了,到如今咱们已经能够在 Variant 的模板构造函数与模板赋值函数里对类型的 copy 和 move 语义进行编译时检查,但对 Variant 自己的 copy 与 move 语义,咱们却一筹莫展了。

variant_t(const variant_t<TS...>& other)
    {
        if (other.type_ == 0) return;

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

由于 Variant 中当前保存的类型 type_ 只有在运行时才能知道,所以若是用户将一个保存了 non-copyable 对象的 Variant 对象赋值给另外一个相同类型的 Variant,此时执行的将会是一个假的拷贝函数,一个运行时的异常将会抛出。

剩下的问题[10.29更新,下面提到的问题已所有解决]

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

参考:

An Implementation of Discriminated Unions in C++