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 种类型)


(未完待续


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM