std::variant 原理研究

不知道 variant 的可以先看一下这个:std::variant - cppreference.com

数据的存储

因为 variant 跟 union 很像,所以我一开始以为 variant 是在内部创建一块足够大(能存放大小最大的类型)的缓冲区,然后通过 placement new 等方法在缓冲区上操作。然后我就发现有问题,variant 是支持 constexpr 的,但 constexpr new 是到 C++20 才被支持的,可是 C++17 就有了 variant。于是我去查找了微软对 variant 的实现。(为了方便阅读,本文对摘取的代码有进行删改)

查看 variant 类的定义,查找其基类,发现了一个叫做 _Variant_storage_ 的类和其别名 _Variant_storage

// _TrivialDestruction 直译过来就是 可平凡地销毁。结合下文代码可以看出是用于进行特化。
template <bool _TrivialDestruction, class... _Types>
class _Variant_storage_ {}; // 这样定义同时可确保在 sizeof...(_Types) 为 0 时该类是空类
template <class... _Types>
using _Variant_storage = _Variant_storage_<conjunction_v<is_trivially_destructible<_Types>...>, _Types...>;

_Variant_storage_ 的实现:

template <class _First, class... _Rest>
class _Variant_storage_<true /*我这里为了方便把两个特化放在一起了*/, _First, _Rest...> {
public:
static constexpr size_t _Size = 1 + sizeof...(_Rest);
union {
_First _Head;
_Variant_storage<_Rest...> _Tail;
};
_Variant_storage_() noexcept {}
template <class... _Types>
constexpr explicit _Variant_storage_(integral_constant<size_t, 0>, _Types&&... _Args)
: _Head(static_cast<_Types&&>(_Args)...) {}
template <size_t _Idx, class... _Types, enable_if_t<(_Idx > 0), int> = 0>
constexpr explicit _Variant_storage_(integral_constant<size_t, _Idx>, _Types&&... _Args)
: _Tail(integral_constant<size_t, _Idx - 1>{}, static_cast<_Types&&>(_Args)...) {}
constexpr _First& _Get() & noexcept {
return _Head;
}
constexpr const _First& _Get() const& noexcept {
return _Head;
}
/*
* 下面 5 个函数是 对于有类型不可平凡销毁的 才有的(即 _TrivialDestruction = true)
*/
~_Variant_storage_() noexcept {};
_Variant_storage_(_Variant_storage_&&) = default;
_Variant_storage_(const _Variant_storage_&) = default;
_Variant_storage_& operator=(_Variant_storage_&&) = default;
_Variant_storage_& operator=(const _Variant_storage_&) = default;
};

可以看到,微软选择通过 union 的递归定义来实现。这样做有以下好处:

  • 使初始化可以在编译期完成。
  • 无需手动计算所需空间大小。
  • 无需手动销毁储存的数据

使用 integral_constant 作为参数决定要初始化第几个类型。显然该操作也是递归进行的。

如果为 0,就初始化 _Head ,否则,将值-1 然后让 _Tail 去初始化。

显然的,取出时也使用递归就行了:

template <size_t _Idx, class _Storage>
constexpr decltype(auto) _Variant_raw_get(_Storage&& _Obj) noexcept {
// 实际上微软在这里直接对 _Idx 进行判断,对某些特定的 _Idx 直接 ._Tail._Tail......_Tail._Get() 手动展开。
// 是为了优化,在非编译期访问时尽可能的减少递归的此数
if constexpr (_Idx == 0) {
return static_cast<_Storage&&>(_Obj)._Get();
} else {
return _Variant_raw_get<_Idx-1>(static_cast<_Storage&&>(_Obj)._Tail);
}
}

类型信息的储存

在没有出错的情况下类型应该是 _Types 中的一种,因此只需要储存其在 _Types 中的索引即可。

获取类型的索引

微软先实现了一个叫做 _Meta_find_index_ 的东西,来获取第一个一样的类型索引。

inline constexpr auto _Meta_npos = ~size_t{0};
template <class _List, class _Ty>
struct _Meta_find_index_ {
using type = integral_constant<size_t, _Meta_npos>; // 不满足要求
};
template <class _List, class _Ty>
using _Meta_find_index = typename _Meta_find_index_<_List, _Ty>::type;
constexpr size_t _Meta_find_index_i_(const bool* const _Ptr, const size_t _Count,
size_t _Idx = 0) {
for (; _Idx < _Count; ++_Idx) {
if (_Ptr[_Idx]) {
return _Idx;
}
}
return _Meta_npos;
}
// *1
template <template <class...> class _List, class _First, class... _Rest, class _Ty>
struct _Meta_find_index_<_List<_First, _Rest...>, _Ty> {
static constexpr bool _Bools[] = {is_same_v<_First, _Ty>, is_same_v<_Rest, _Ty>...};
using type = integral_constant<size_t, _Meta_find_index_i_(_Bools, 1 + sizeof...(_Rest))>;
};

解释一下上面代码:

_List:要求是 template <class...> 类型的。相当于把多个类型打包起来当作一个新的类型,这样就可以把 _Ty 放到后面,而且在使用时也可以直接使用 variant 的类型(variant<...>满足这点)。

type:储存编号(我不能理解微软为什么不直接使用 static constexpr size_t value = ...

如果 _List 是有效的,那么就会由 *1 处理:

  1. 对所有的类型进行比较,将结果存放到 _Bools 中。
  2. _Bools 中进行查找。

有人可能会问,为什么不直接递归查找:

template<typename _list, typename _t>
struct _meta_find_idx_
{
using type = integral_constant<size_t, -1>;
};
template <class _list, class _t>
using _meta_find_idx = typename _meta_find_idx_<_list, _t>::type;
template<template<typename...> typename _list, typename _first, typename..._rest, typename _t>
struct _meta_find_idx_<_list<_first, _rest...>, _t>
{
using type = conditional_t < is_same_v<_t, _first>,
integral_constant<size_t, 0>,
integral_constant<size_t, _meta_find_idx<_list<_rest...>, _t>::value + 1>
>;
};

_Variant_raw_get 的原因差不多,也是为了优化,由于 C++ 的 RTTI 机制,该操作可能是在非编译期执行的。

接下来微软实现了一个 _Meta_find_unique_index 来确保当有且只有一个类型与目标相同时才能得到索引:

template <class _List, class _Ty>
struct _Meta_find_unique_index_ {
using type = integral_constant<size_t, _Meta_npos>;
};
template <class _List, class _Ty>
using _Meta_find_unique_index =
// 如果它恰好出现一次,则为 _Ty 在 _List 中的索引,否则为 _Meta_npos
typename _Meta_find_unique_index_<_List, _Ty>::type;
constexpr size_t _Meta_find_unique_index_i_2(const bool* const _Ptr, const size_t _Count,
const size_t
_First) { // 如果没有 _First < j < _Count 使得 _Ptr[j] 为真(后面没有相同的)则返回 _First,
// 否则返回 _Meta_npos
return (_First != _Meta_npos
&& _Meta_find_index_i_(_Ptr, _Count, _First + 1) == _Meta_npos)
? _First : _Meta_npos;
}
constexpr size_t _Meta_find_unique_index_i_(const bool* const _Ptr,
const size_t _Count) { // 先找到最前索引,然后将它作为参数传给 _Meta_find_unique_index_i_2
return _Meta_find_unique_index_i_2(_Ptr, _Count, _Meta_find_index_i_(_Ptr, _Count));
}
template <template <class...> class _List, class _First, class... _Rest, class _Ty>
struct _Meta_find_unique_index_<_List<_First, _Rest...>, _Ty> {
using type = integral_constant<size_t,
_Meta_find_unique_index_i_(
_Meta_find_index_<_List<_First, _Rest...>, _Ty>::_Bools,
1 + sizeof...(_Rest)
)
>;
};

这样,就可以避免类型中存在相同类型时的歧义操作 (直接不让操作)

储存类型的索引

又是微软的奇妙优化:

template <size_t _Count>
using _Variant_index_t = // 选择有符号类型以便将 -1 转换为 size_t 时可以廉价地进行符号扩展
conditional_t<(_Count < static_cast<size_t>((numeric_limits<signed char>::max)())), signed char,
conditional_t<(_Count < static_cast<size_t>((numeric_limits<short>::max)())), short, int>>;

根据类型数选择储存的类型。(我并不认为有人真的会用到超过 128 种类型)


(未完待续

posted @   IpnAh  阅读(1351)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示