不知道 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 種類型)
(未完待續