【转载】用 C++11 打造好用的 variant (上)

277 阅读4分钟

参考原文地址:(原创)用 C++11 打造好用的 variant(原创)用 C++11 打造好用的 variant(更新)

我对文章的格式和错别字进行了调整,并标注出了重要的部分。以下是正文。

正文

variant 类似于 union,它能代表定义的多种类型,允许将不同类型的值赋给它。它的具体类型是在初始化赋值时确定。boost 中的 variant 的基本用法:

typedef variant<int, char, double> vt;
vt v = 1;
v = '2';
v = 12.32;

用 variant 一个好处是可以擦除类型,不同类型的值都统一成一个 variant ,虽然这个 variant 只能存放已定义的类型,但这在很多时候已经够用了。 取值的时候,通过 get(v) 来获取真实值。然而,当 T 类型与 v 的类型不匹配时,会抛出一个 bad_cast 的异常来。boost 的 variant 抛出的异常往往没有更多的信息,不知道到底是哪个类型转换失败,导致发生异常调试时很不方便。因此,就考虑用 C++11 去实现一个 variant , 这个 variant 可以很容易知道取值时,是什么类型转换失败了。

打造 variant 需要解决的问题

一、要在内部定义一个 char 缓冲区

缓冲区用来存放 variant 的值,这个值是 variant 定义的多种类型中的某种类型的值,因此,这个缓冲区要足够大,能够存放类型最大(sizeof(Type))的值才可以,这个缓冲区的大小还必须在编译期计算出来。因此需要首先要解决的是 variant 值存放的缓冲区定义的问题。

二、要解决赋值的问题

将值赋给 vairiant 时,需要将该值的类型 ID 记录下来,以便在后面根据类型取值。将值保存到内部缓冲区时,还需要用 placement new 在缓冲区创建对象。另外,还要解决一个问题,就是赋值时需要检查 variant 中已定义的类型中是否含有该类型,如果没有则编译不通过,以保证赋值是合法的。

三、要解决取值的问题

通过类型取值时,要判断类型是否匹配,如果不匹配,将详情打印出来,方便调试。

打造 variant 的关键技术

找出最大的 typesize

第一个需要解决的问题是如何找出多种类型中,size 最大的那个类型的 size 。看看如何从多种类型中找出最大类型的 size 。

template <typename T, typename... Args>
struct MaxType : std::integral_constant<int,
                                        (sizeof(T) > MaxType<Args...>::value ? sizeof(T) : MaxType<Args...>::value)>

{
};

template <typename T>
struct MaxType<T> : std::integral_constant<int, sizeof(T)>
{
};

通过这个 MaxType 就可以在编译期获取类型中最大的 maxsize 了: MaxType<Types...>::value 。

类型检查和缓冲区中创建对象

第二个需要解决两个问题:

  • 检查赋值的类型是否在已定义的类型中;
  • 在缓冲区中创建对象及析构;

先看看如何判断类型列表中是否含有某种类型:

template <typename T, typename... List>
struct Contains : std::true_type
{
};

template <typename T, typename Head, typename... Rest>
struct Contains<T, Head, Rest...>
    : std::conditional<std::is_same<T, Head>::value, 
                       std::true_type,
                       Contains<T, Rest...>>::type
{
};

template <typename T>
struct Contains<T> : std::false_type
{
};

通过 bool 值 Contains<T, Types>::vaule 就可以判断是否含有某种类型。

再看看如何在缓冲区中创建对象

通过 placement new 在该缓冲区上创建对象,new(data) T(value); 其中 data 表示一个 char 缓冲区,T 表示某种类型。在缓冲区上创建的对象还必须通过 ~T 去析构,因此还需要一个析构 vairiant 的帮助类:

template <typename... Args>
struct VariantHelper;

template <typename T, typename... Args>
struct VariantHelper<T, Args...>
{
    inline static void Destroy(type_index id, void *data)
    {
        if (id == type_index(typeid(T)))
            ((T *)(data))->~T();
        else
            VariantHelper<Args...>::Destroy(id, data);
    }
};

template <>
struct VariantHelper<>
{
    inline static void Destroy(type_index id, void *data) {}
};

通过 VariantHelper::Destroy 函数就可以析构 variant 了。

取值问题

第三个需要解决取值问题,如果发生异常,就打印出详细信息。这个就比较简单了,看后面的实现代码就行了。

C++11 中完整的 variant 是如何实现的

#include <typeindex>
#include <iostream>
#include <type_traits>
using namespace std;

template <typename T, typename... Args>
struct MaxType : std::integral_constant<int,
                                        (sizeof(T) > MaxType<Args...>::value ? sizeof(T) : MaxType<Args...>::value)>

{
};

template <typename T>
struct MaxType<T> : std::integral_constant<int, sizeof(T)>
{
};

template <typename T, typename... List>
struct Contains : std::true_type
{
};

template <typename T, typename Head, typename... Rest>
struct Contains<T, Head, Rest...>
    : std::conditional<std::is_same<T, Head>::value, std::true_type, Contains<T,

                                                                              Rest...>>::type
{
};

template <typename T>
struct Contains<T> : std::false_type
{
};

template <typename... Args>
struct VariantHelper;

template <typename T, typename... Args>
struct VariantHelper<T, Args...>
{
    inline static void Destroy(type_index id, void *data)
    {
        if (id == type_index(typeid(T)))
            ((T *)(data))->~T();
        else
            VariantHelper<Args...>::Destroy(id, data);
    }
};

template <>
struct VariantHelper<>
{
    inline static void Destroy(type_index id, void *data) {}
};

template <typename... Types>
class Variant
{
    typedef VariantHelper<Types...> Helper_t;

public:
    Variant(void) : m_typeIndex(typeid(void))
    {
    }

    ~Variant()
    {
        Helper_t::Destroy(m_typeIndex, &m_data);
    }

    template <typename T>
    bool Is()
    {
        return (m_typeIndex == type_index(typeid(T)));
    }

    template <typename T>
    T &Get()
    {
        if (!Is<T>())
        {
            cout << typeid(T).name() << " is not defined. "
                 << "current type is " <<

                m_typeIndex.name() << endl;
            throw std::bad_cast();
        }

        return *(T *)(&m_data);
    }

    template <class T,
              class = typename std::enable_if<Contains<typename

                                                       std::remove_reference<T>::type,
                                                       Types...>::value>::type>
    Variant(T &&value) : m_typeIndex(type_index(typeid(void)))
    {
        Helper_t::Destroy(m_typeIndex, &m_data);
        typedef typename std::remove_reference<T>::type U;
        new (m_data) U(std::forward<T>(value));
             m_typeIndex = type_index(typeid(T));
         
    }
    template <typename F>
    void Visit(F &&f)
    {
          using T = typename function_traits<F>::arg<0>::type;
          if (Is<T>())			    f(Get<T>());
    }
    template <typename F, typename... Rest>
    void Visit(F &&f, Rest &&...rest)
    {
          using T = typename function_traits<F>::arg<0>::type;
          if (Is<T>())			    Visit(std::forward<F>(f));
          else     Visit(std::forward<Rest>(rest)...);
    }

private:
    char m_data[MaxType<Types...>::value];
    std::type_index m_typeIndex;
};

测试代码

void TestVariant()
{
    typedef Variant<int, char, double> cv;
    int x = 10;

    cv v = x;
    v = 1;
    v = 1.123;
    v = "";          //compile error
    v.Get<int>();    //1
    v.Get<double>(); //1.23
    v.Get<short>();  //exception: short is not defined. current type is int
    v.Is<int>();     //true
}

总结

C++11 实现的 variant 在用法上和 boost.variant 保持一致,但实现更简洁,50 行代码搞定。而且还能在抛出异常时提示详细信息,方便调试。