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