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
处理:
- 对所有的类型进行比较,将结果存放到
_Bools
中。 - 在
_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 种类型)
(未完待续
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了