variant again

之前写过一个FakeVariant,利用Loki中提供的多重继承实现的,与其说是variant,倒不如tuple贴切,因为这个FakeVariant和tuple一样同时保存了所有给定的类型,并且占用的大小也是所有给定类型的和,无论你有没有使用这个空间。至于为什么又想造一个vairant的轮子,首先是因为FakeVariant占用空间太大,且没有构造函数;其次是因为C++17提供的variant太难用了。那么为什么不试一试boost.variant呢? 因为要链接一个boost库实在是不喜欢,最重要的是C++17的variant就是boost那里抄过来的!所以,打算再造一个variant,哪怕只有简单的set/get功能,至于copy/move以及关系比较都不是重点。

一个使用variant的典型场景

std::map<string, ???> moha { {"name", "elder"}, { "age", 233} };

这里的???显然必须包含string以及int类型,那么可以写成这样variant<string, int>

个人认为一个基本的理想的variant应该是这样的

template<typename T, typename... Rest>
class variant
{
  public:
    template<typename U> variant(const U& x);
    ~variant();
    template<typename U> set(const U& x);
    template<typename U> T& get();
};

其中没有包含复制构造以及移动构造和复制比较等操作。

下面就针对前面那个场景做一个简单的实现。 首先需要确定用什么作为储存,是栈上还是堆上,是raw memory还是char[??]。幸运的是,不用这么纠结,因为C++11提供的aligned_storage会创建栈上的未初始化的内存,只需提供空间大小以及对齐大小即可。

typename std::aligned_storage<data_size, align_size>::type data_;

那么,如果我们有一堆的可选类型,如何获取data_sizealign_size呢?显然,要容纳众多类型又要节约空间,当然是需要众多类型中size最大的那个,一个align最大的那个咯。

// size less than
template<typename T, typename U> struct size_lt
{
  constexpr static bool value = sizeof(T) < sizeof(U);
};

// size greater than
template<typename T, typename U> struct size_gt
{
  constexpr static bool value = sizeof(T) > sizeof(U);
};

template<template<typename, typename> class, typename, typename...> struct type_size;
template<template<typename, typename> class Op, typename T> struct type_size<Op, T>
{
  constexpr static size_t value = sizeof(T);
};

template<template<typename, typename> class Op, typename T, typename U, typename... Rest>
struct type_size<Op, T, U, Rest...>
{
  constexpr static size_t value = Op<T, U>::value
                                  ? type_size<Op, T, Rest...>::value
                                  : type_size<Op, U, Rest...>::value;
};

template<size_t _1, size_t...> struct max_size_of;
template<size_t _1> struct max_size_of<_1>
{
  constexpr static size_t value = _1;
};
template<size_t _1, size_t _2, size_t... Rest>
struct max_size_of<_1, _2, Rest...>
{
  constexpr static size_t value = _1 > _2 ? max_size_of<_1, Rest...>::value
                                          : max_size_of<_2, Rest...>::value;
};

有了这几个辅助的traits,我们就可以写出具体的储存类型了

typename std::aligned_storage<type_size<size_gt, Rest...>::value, max_size_of<alignof(Rest)...>::value>::type data_;

我们已经有了储存类型,那么我们尝试完成variant的构造,C++提供了placement new,我们可以在上面这个data_中构造需要的对象,但是有一个问题,如果直接使用placement new的话,编译器会抱怨new没有针对我们的储存类型特化,从而结束编译。因此,我们需要显示的将这块栈空间转化成new可以识别的类型

template<typename T> variant(const T& x)
{
  static_assert(is_in<T, Rest...>::value, "type is invalid");
  new(static_cast<void*>(&data_)) T(rhs);
} 

可以看到,我们将aligned_storage的指针转成了void*,另外,我还必须检查构造的类型是不是我们定义的众多类型中的一个,下面是这个检查的实现

template<typename T, typename... Types> struct is_in;
template<typename T> struct is_in<T>
{
  constexpr static bool value = false;
};
template<typename T, typename _1ST, typename... Rest>
struct is_in<T, _1ST, Rest...>
{
  private:
    constexpr static bool tmp = std::is_same<T, _1ST>::value;
  public:
    constexpr static bool value = tmp ? tmp : is_in<T, Rest...>::value;
};

注意,无论是set/get都需要这样一个检查!

下面是一个简单的get操作

template<typename T> T& get()
{
  static_assert(is_in<T, Rest...>::value, "type is invalid");
  return return *reinterpret_cast<T*>(&data_);
}

如果你稍微注意一下,就会发现,上面的get实现是存在问题的! 因为我们的variant是存在默认构造函数的,如果没有事先执行set操作,那么这里的get操作可能不会得到期望的结果!类似的,虽然这里还没有实现析构函数以及set函数,但是我们可以预见到,我们不得不保存一个是否已经初始化的标志以及保持进行这个初始化是所用的类型!而且,我们还必须考虑是否允许下面的做法

variant<int, string> va{233};
cout << va.get<int>() << '\n';
va.set<string>("233");
cout << va.get<string>() << '\n';

如果不允许以上代码的操作,那么这个所谓的variant就没啥用处了,所以,我们只能想办法在set和析构函数中正确的调用最后一个set操作所用类型的析构函数。当然,这也并非不可能!下面是剩下的部分实现

~variant()
{
  helper::clear(type_index_, &data_);
}

template<typename T> void set(const T& x)
{
  static_assert(is_in<T, Rest...>::value, "type is invalid");
  helper::clear(type_index_, &data_);
  new(static_cast<void*>(&data_)) T(rhs);
  type_index_ = index_of_type<T, Rest...>::value;
}

为了实现正确的析构,我们需要一个type_index_,这个索引指出我们执行set操作时所用类型在Rest...中的位置,然后我们还需要一个帮助函数helper::clear,这个函数根据类型的索引在Rest...中找出对应的类型,然后正确的执行该类型的析构函数。下面是这几个辅助工具的实现

template<typename, typename...> struct index_of_type;
template<typename T> struct index_of_type<T>
{
  constexpr static int value = 0;
};
template<typename T, typename U, typename... Rest> struct index_of_type<T, U, Rest...>
{
  private:
    constexpr static bool tmp = std::is_same<T, U>::value;
  public:
    constexpr static int value = tmp ? 0 : 1 + index_of_type<T, Rest...>::value;
};

template<typename...> struct variant_helper;
template<> struct variant_helper<>
{
  static void clear(size_t, void*) {}
};
template<typename T, typename... R> struct variant_helper<T, R...>
{
  static void clear(int index, void* data)
  {
    if(index > 0)
    {
      index -= 1;
      variant_helper<R...>::clear(index, data);
    }
    else if(index == 0)
    {
      reinterpret_cast<T*>(data)->~T();
    }
  }
};

上面的实现同样有一个问题,如果T是POD类型,那么调用~T()有什么后果呢?是否需要检查一下T是否是POD类型呢?
放下这个疑问,我们先编译试一试这个简单的variant实现

variant_impl

从运行结果来看,对于POD类型(int),似乎并没有问题发生。当然,加上std::is_pod检查一下更为保险一些(毕竟没有去翻C++标准…)

通过查阅标准(实际上是C++14标准的最后一个草案)找到如下段落
pseudo-destructor
以上文字表明,标准允许对一个scalar纯量使用析构函数(伪析构函数),即使它没有析构函数,比如这样

using T = int;
int main()
{
  I x = 10;
  x.I::~I();
  x = 20;
}

因此,对于POD类型,在模板函数中对其使用析构函数并不会起作用,所以,判断是否为POD类型的操作可有可无。

至此,一个简单的variant就完成了,完整代码见GitHub master dev

除了可能会支持copy/move构造(赋值),以及operator<operator==等外,还需要考虑其它的一些情况

已搞定,接下来可以看看标准,查一查有无UB。。。


另见: https://isliberty.me/simple_variant