1. 前言
std::variant是C++17标准库引入的一种类型,用于安全地存储和访问多种类型中的一种。它类似于C语言中的联合体(union),但功能更为强大。与联合体相比,std::variant具有类型安全性,可以判断当前存储的实际类型,并且可以存储结构体/类等复杂的数据结构。
2. preview 原理
我们依然采用“一图胜千言”的思想,给大家先展现下std::variant对应的UML图。这些图都是用我之前写的工具DotObject自动画出来的,有兴趣请参考《GDB调试技巧实战–自动化画出类关系图》,还有一篇应用实践《Modern C++利用工具快速理解std::tuple的实现原理》。
我们先举个简单的例子, 看下variant的庐山真面目。
std::variant<int,double>
3. std::variant的实现重点:存储
通过上面的preview,相信读者已经通过直观的认识快速入门并理解了底层是如何存储数据的了。
解释一下:
(1)重点是_Variadic_union, 它是一个递归union,大概相当于c中的:
union _Variadic_union{
数据 _M_first; //第N层的
_Variadic_union _M_rest; //下一层还是一个union
}
(2) 另一个重点是:_Variant_storage::_M_index 是当前数据类型是可选类型列表中第几个,比如设置一个0.2,则当前类型是double, 此时_M_index=1(从0开始)。
下面我们用GDB把数据打印出来看看:
variant<int,double> v1(3);
再赋值为2.0
v1 = 2.0;
在有了直观认识后,我们来看下源代码:
348 template<typename... _Types>
349 union _Variadic_union { };
350
351 template<typename _First, typename... _Rest>
352 union _Variadic_union<_First, _Rest...>
353 {
354 constexpr _Variadic_union() : _M_rest() { }
355
356 template<typename... _Args>
357 constexpr _Variadic_union(in_place_index_t<0>, _Args&&... __args)
358 : _M_first(in_place_index<0>, std::forward<_Args>(__args)...)
359 { }
360
361 template<size_t _Np, typename... _Args>
362 constexpr _Variadic_union(in_place_index_t<_Np>, _Args&&... __args)
363 : _M_rest(in_place_index<_Np-1>, std::forward<_Args>(__args)...)
364 { }
365
366 _Uninitialized<_First> _M_first;
367 _Variadic_union<_Rest...> _M_rest;
368 };
先不必管行356到364(问题一,这几行干啥用?),367行体现了递归的思想(递归在标准库实现中大量使用),每次都把第一个值单独拿出来。如果理解有困难,直接扔到cppinsights让它帮我们展开(为了方便cppinsights展开,我把_M_first先简化为_First类型了,之后再详细分析它):
可以看到
_Variadic_union<int,double> = int _M_first + _Variadic_union _M_rest
_Variadic_union = double _M_first + _Variadic_union<>
OK, _M_rest是为了递归,那 _M_first 哪?当然,大家已经看到它对应每层的数据(int/double),不过它的实际类型是 _Uninitialized,在我们的例子中分别对应只包含int或double的结构体,
不要简单的以为_Uninitialized总是“类型 _M_storage”, 看下存结构体或类会怎么样?
class Person{
public:
Person(const string& name, int age):_name(name),_age(age){}
~Person(){
cout<<"Decons Person:";
print();
}
void print(){
cout<<"name="<<_name<<" age="<<_age<<endl;
}
private:
string _name;
int _age;
};
int main(){
variant<int,Person> v2;
}
可见它把Person变成了char[40], 40恰为Person的size大小。
让我们看下**_Uninitialized**的定义:
227 // _Uninitialized<T> is guaranteed to be a trivially destructible type,
228 // even if T is not.
229 template<typename _Type, bool = std::is_trivially_destructible_v<_Type>>
230 struct _Uninitialized;
231
232 template<typename _Type>
233 struct _Uninitialized<_Type, true>
234 {
235 template<typename... _Args>
236 constexpr
237 _Uninitialized(in_place_index_t<0>, _Args&&... __args)
238 : _M_storage(std::forward<_Args>(__args)...)
239 { }
240
241 constexpr const _Type& _M_get() const & noexcept
242 { return _M_storage; }
243
244 constexpr _Type& _M_get() & noexcept
245 { return _M_storage; }
246
247 constexpr const _Type&& _M_get() const && noexcept
248 { return std::move(_M_storage); }
249
250 constexpr _Type&& _M_get() && noexcept
251 { return std::move(_M_storage); }
252
253 _Type _M_storage;
254 };
255
256 template<typename _Type>
257 struct _Uninitialized<_Type, false>
258 {
259 template<typename... _Args>
260 constexpr
261 _Uninitialized(in_place_index_t<0>, _Args&&... __args)
262 {
263 ::new ((void*)std::addressof(_M_storage))
264 _Type(std::forward<_Args>(__args)...);
265 }
266
267 const _Type& _M_get() const & noexcept
268 { return *_M_storage._M_ptr(); }
269
270 _Type& _M_get() & noexcept
271 { return *_M_storage._M_ptr(); }
272
273 const _Type&& _M_get() const && noexcept
274 { return std::move(*_M_storage._M_ptr()); }
275
276 _Type&& _M_get() && noexcept
277 { return std::move(*_M_storage._M_ptr()); }
278
279 __gnu_cxx::__aligned_membuf<_Type> _M_storage;
280 };
很明显,针对is_trivially_destructible_v是true、false各有一个特化,type为Person时命中_Uninitialized<_Type, false>(因为它有自己定义的析构函数,故is_trivially_destructible_v==false, 细节请参考下面的截图)
4. std::variant的实现重点:get(获取值)
存是递归,取也是递归取
给出任意一个variant object, 比如v1, 我们知道
- 数据类型对应的下标是v1._M_index
- 数据存在v1._M_u
则要想获得第一个数据类型的值只需return v1._M_u._M_first
要想获得第二个数据类型的值只需return v1._M_u._M_rest._M_first
要想获得第三个数据类型的值只需return v1._M_u._M_rest._M_rest._M_first
… …
这正是源代码的实现方式:
282 template<typename _Union>
283 constexpr decltype(auto) //获得第一个数据类型的值 我们的例子中是int
284 __get(in_place_index_t<0>, _Union&& __u) noexcept
285 { return std::forward<_Union>(__u)._M_first._M_get(); }
286
287 template<size_t _Np, typename _Union>
288 constexpr decltype(auto) //获得第N个数据类型的值 我们的例子中第二个是double
289 __get(in_place_index_t<_Np>, _Union&& __u) noexcept
290 {
291 return __variant::__get(in_place_index<_Np-1>, //递归
292 std::forward<_Union>(__u)._M_rest);
293 }
294
295 // Returns the typed storage for __v.
296 template<size_t _Np, typename _Variant>
297 constexpr decltype(auto)
298 __get(_Variant&& __v) noexcept
299 {
300 return __variant::__get(std::in_place_index<_Np>,
301 std::forward<_Variant>(__v)._M_u);
302 }
对照上面的实现想一想下面的代码如何运行的?
variant<int,double> v(1.0);
cout<<get<1>(v);
这个哪?
std::variant<int, double, char, string> myVariant("mzhai");
string s = get<3>(myVariant);
需要很多次._M_rest对不? 所以如果你非常看重效率,那么请把常用的类型安排在前面,比如把上面的代码改成:
std::variant<string, int, double, char> myVariant("mzhai");
后来注释,这是debug的情况,开启优化后没有性能问题。 请参考 《Modern C++ std::variant 我小看了get的速度》。
5. std::variant的实现重点:赋值
赋值大体有三种办法:
- 初始化(调用构造函数)
- 重新赋值 (调用operator = )
- 重新赋值 (调用emplace)
但赋值很复杂,因为情况很多:
- variant alternatives都是int般简单类型,=右边也是简单类型
- variant alternatives都是trivial类,=右边也是trivial类
- variant alternatives都是非trivial类,=右边也是非trivial类
- variant alternatives都是非trivial类,=右边是构造非trivial类的参数
- variant alternatives都是非trivial类,而且有些类的ctor或mtor或assignment operator被删除
- copy assignment/ move assignment 会抛异常导致valueless
- …
情况多的不厌其烦。头大。
我们只挑最简单的说下(捏个软柿子~), 考虑如下代码:
std::variant<int, double> v;
v = 2.0f;
对应的实现为:
1456 template<typename _Tp>
1457 enable_if_t<__exactly_once<__accepted_type<_Tp&&>>
1458 && is_constructible_v<__accepted_type<_Tp&&>, _Tp>
1459 && is_assignable_v<__accepted_type<_Tp&&>&, _Tp>,
1460 variant&>
1461 operator=(_Tp&& __rhs)
1462 noexcept(is_nothrow_assignable_v<__accepted_type<_Tp&&>&, _Tp>
1463 && is_nothrow_constructible_v<__accepted_type<_Tp&&>, _Tp>)
1464 {
1465 constexpr auto __index = __accepted_index<_Tp>;
1466 if (index() == __index)
//index()为0,因为初始化v时以int初始化,__index为1
//这种情况对应的是前面那次赋值和这次赋值类型一样。比如v=1.0; v=2.0
1467 std::get<__index>(*this) = std::forward<_Tp>(__rhs);
1468 else
1469 {//前后两次赋值类型不一样,比如v=1; v=2.0. 本例v=2.0f走这里。
1470 using _Tj = __accepted_type<_Tp&&>;
1471 if constexpr (is_nothrow_constructible_v<_Tj, _Tp>
1472 || !is_nothrow_move_constructible_v<_Tj>)
1473 this->emplace<__index>(std::forward<_Tp>(__rhs));//本例走这
1474 else
1475 operator=(variant(std::forward<_Tp>(__rhs)));
1476 }
1477 return *this;
1478 }
1499 template<size_t _Np, typename... _Args>
1500 enable_if_t<is_constructible_v<variant_alternative_t<_Np, variant>,
1501 _Args...>,
1502 variant_alternative_t<_Np, variant>&>
1503 emplace(_Args&&... __args)
1504 {
1505 static_assert(_Np < sizeof...(_Types),
1506 "The index must be in [0, number of alternatives)");
1507 using type = variant_alternative_t<_Np, variant>;
1508 namespace __variant = std::__detail::__variant;
1509 // Provide the strong exception-safety guarantee when possible,
1510 // to avoid becoming valueless.
1511 if constexpr (is_nothrow_constructible_v<type, _Args...>)
1512 {
1513 this->_M_reset(); //析构原对象,并置_M_index=-1
1514 __variant::__construct_by_index<_Np>(*this, //placement new,构造新值
1515 std::forward<_Args>(__args)...);
1516 }
1517 else if constexpr (is_scalar_v<type>)
1518 {
1519 // This might invoke a potentially-throwing conversion operator:
1520 const type __tmp(std::forward<_Args>(__args)...);
1521 // But these steps won't throw:
1522 this->_M_reset();
1523 __variant::__construct_by_index<_Np>(*this, __tmp);
1524 }
1525 else if constexpr (__variant::_Never_valueless_alt<type>()
1526 && _Traits::_S_move_assign)
析构原来的类型对象和构造新的类型对象请分别参考_M_reset __construct_by_index
422 void _M_reset()
423 {
424 if (!_M_valid()) [[unlikely]]
425 return;
426
427 std::__do_visit<void>([](auto&& __this_mem) mutable
428 {
429 std::_Destroy(std::__addressof(__this_mem));
430 }, __variant_cast<_Types...>(*this));
431
432 _M_index = static_cast<__index_type>(variant_npos);
433 }
1092 template<size_t _Np, typename _Variant, typename... _Args>
1093 inline void
1094 __construct_by_index(_Variant& __v, _Args&&... __args)
1095 {
1096 auto&& __storage = __detail::__variant::__get<_Np>(__v);
1097 ::new ((void*)std::addressof(__storage))
1098 remove_reference_t<decltype(__storage)>
1099 (std::forward<_Args>(__args)...);
1100 // Construction didn't throw, so can set the new index now:
1101 __v._M_index = _Np;
1102 }
赋值原理基本大体如此,如有读者感觉意犹未尽,这里我给一个程序供大家调试研究思考:
#include<iostream>
#include<variant>
using namespace std;
int main(){
class C1{
public:
C1(int i):_i(i){}
private:
int _i;
};
cout<<is_nothrow_constructible_v<C1><<endl;
cout<<is_nothrow_move_constructible_v<C1><<endl;
variant<string, C1> v;
v = 10; //重点在这里
return 0;
}
提示:
- 没走emplace, 走了1475 operator=(variant(std::forward<_Tp>(__rhs)));
- 还记得上面我们留了一个问题吗?
先不必管行356到364(问题一,这几行干啥用?)
本例调用了362的构造函数 constexpr _Variadic_union(in_place_index_t<_Np>, _Args&&… __args)
这个构造函数就是为类类型(有parameterized constructor)准备的。