1. 前言
std::any
是 C++17 中引入的一个新特性,它是一个类型安全的容器,可以在其中存储任何类型(但此类型必须可拷贝构造)的值,包括基本类型、自定义类型、指针等。相比于void*
指针,std::any
更为类型安全,可以避免由于类型转换错误而导致的运行时错误。
std::any
获取值时需要指定正确的类型。如果尝试获取的类型与存储的类型不匹配,将抛出 std::bad_any_cast
异常。
2. std::any初体验
cppreference上就有一个例子,copy过来供大家学习:
#include <any>
#include <iostream>int main()
{std::cout << std::boolalpha;// any typestd::any a = 1;std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';a = 3.14;std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';a = true;std::cout << a.type().name() << ": " << std::any_cast<bool>(a) << '\n';// bad casttry{a = 1;std::cout << std::any_cast<float>(a) << '\n';}catch (const std::bad_any_cast& e){std::cout << e.what() << '\n';}// has valuea = 2;if (a.has_value())std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';// reseta.reset();if (!a.has_value())std::cout << "no value\n";// pointer to contained dataa = 3;int* i = std::any_cast<int>(&a);std::cout << *i << '\n';
}
输出:
int: 1 double: 3.14 bool: true bad any_cast int: 2 no value 3
正如介绍中所说:
std::any可以有值,可以无值,还能存储int/double/bool等等类型数据,获取值时如果类型不对会抛出异常。
3. 原理-存储
any的存储是所有container中最简单的一个了,全部实现才600来行,通读一点问题没有。
any就是一个普通的类,没有任何模板参数,其 实现 在文件/usr/include/c++/11/any中。
class std::any {private:void (*_M_manager)(std::any::_Op, const std::any *, std::any::_Arg *);std::any::_Storage _M_storage;
}union std::any::_Storage {void *_M_ptr;std::aligned_storage<8, 8>::type _M_buffer; //size与指针相同,要求8字节对齐
}
std::any只有两项数据成员:
- _M_storage:数据存在这。因为用到了小对象优化,所以是一个union,笼统的讲:小对象用栈空间_M_buffer, 大点的对象要在堆上分配空间,由_M_ptr指向,不过这么讲不是很准确。
- _M_manager:一个特殊的函数,用来操作_M_storage,操作类型包括:访问、克隆(针对any copy)、移动(针对move语义)等等。
本节着重于第一条_M_storage,第二条我们在后面的“原理-初始化” 和 “原理-获取数据 ”中会详细叙述。
3.1 存储-直观认识
关于_M_storage的内容,我们先看两个实例,从例子中有个直观认识:
std::any a = 1;
_M_ptr=1肯定不合法,1是按字节存入了char _M_buffer[8], 我的机器因为是小端故第一个字节是\001. 所以这里用了小对象优化。
再给a赋值为一个字符串,看看8个字节存不下的情况:
a = string("mzhai");
可以看到new出了新空间,在堆上存储了string “mzhai”,由_M_ptr指向它。
3.2 存储-源码
OK,让我们看看源码,它是如何判断用不用小对象优化的哪?
94 template<typename _Tp, typename _Safe = is_nothrow_move_constructible<_Tp>,95 bool _Fits = (sizeof(_Tp) <= sizeof(_Storage))96 && (alignof(_Tp) <= alignof(_Storage))>97 using _Internal = std::integral_constant<bool, _Safe::value && _Fits>;
_Internal见名知意,用“内部”栈空间的意思,其值由两点确定:
- _Tp必须是可移动构造 且 不抛异常的。
- _Tp的size必须小于预留的栈空间大小(8字节),且对齐要求小于等于8。
4. 原理-初始化
现在我们已经知道了any有两个重要的成员变量 以及 数据存在哪。我们想进一步看看如何由别的数据类型初始化一个any对象哪?这两个成员变量都是如何赋的值哪?
除了默认构造函数还有四种方式初始化一个any对象:
any(_Tp&& __value) //调用_Tp copy ctor/move ctor
any(in_place_type_t<_Tp>, _Args&&... __args) //调用_Tp parameterized ctor
any(in_place_type_t<_Tp>, initializer_list<_Up> __il, _Args&&... __args) //调用_Tp parameterized ctoroperator=(_Tp&& __rhs)
如果你不太熟悉前三种用法,请参考官方链接。
一旦初始化了一个any对象,就可以用它的本身的copy ctor/move ctor来初始化别的any对象,所以上面这四个是基础。
前三个很相似,我是指它们的流程,流程分三步:
1. 它们都有激活的条件:至少要求Tp是可拷贝构造的
比如第一个要求:is_copy_constructible<_VTp>
后面的要求更多,不仅仅要求is_copy_constructible,在此不再展开介绍。
2. 初始化_M_manager:它是一个函数指针,要么指向小对象处理函数_Manager_internal::_S_manage,要么指向非小对象处理函数_Manager_external::_S_manage。
template <typename _Tp, typename _VTp = _Decay_if_not_any<_Tp>,typename _Mgr = _Manager<_VTp>,typename = _Require<__not_<__is_in_place_type<_VTp>>,is_copy_constructible<_VTp>>>any(_Tp&& __value): _M_manager(&_Mgr::_S_manage)template<typename _Tp, typename _Safe = is_nothrow_move_constructible<_Tp>,bool _Fits = (sizeof(_Tp) <= sizeof(_Storage))&& (alignof(_Tp) <= alignof(_Storage))>using _Internal = std::integral_constant<bool, _Safe::value && _Fits>;template<typename _Tp>struct _Manager_internal; // uses small-object optimizationtemplate<typename _Tp>struct _Manager_external; // creates contained object on the heap//根据前面一节"原理-存储"中介绍的判断来决定用不用小对象优化template<typename _Tp>using _Manager = conditional_t<_Internal<_Tp>::value,_Manager_internal<_Tp>,_Manager_external<_Tp>>;
无论_Manager_internal还是_Manager_external 都定义了四个相同的静态函数:
- _S_manage(_Op __which, const any* __anyp, _Arg* __arg);
- _S_create(_Storage& __storage, _Up&& __value) //下面的“存入数据”会用到
- _S_create(_Storage& __storage, _Args&&... __args) //下面的“存入数据”会用到
- _S_access(const _Storage& __storage) //any_cast会用到
我们的关注点在_S_manage,简单比较下_Manager_internal::_S_manage 与 _Manager_external::_S_manage的不同:
template<typename _Tp> any::_Manager_internal<_Tp>::_S_manage(_Op __which, const any* __any, _Arg* __arg){// 从char _M_storage._M_buffer[8]中取数据auto __ptr = reinterpret_cast<const _Tp*>(&__any->_M_storage._M_buffer);switch (__which){case _Op_access:__arg->_M_obj = const_cast<_Tp*>(__ptr);break;template<typename _Tp>voidany::_Manager_external<_Tp>::_S_manage(_Op __which, const any* __any, _Arg* __arg){// 从_M_storage._M_ptr指向的内存中取数据auto __ptr = static_cast<const _Tp*>(__any->_M_storage._M_ptr);switch (__which){case _Op_access:__arg->_M_obj = const_cast<_Tp*>(__ptr);break;
3. 存入数据
any中的两大数据成员之一_M_manager已经搞定,下一步就是_M_storage。
_M_storage的初始化,无论是三种初始化中的哪种,都是委托给_Mgr::_S_create完成的。比如第一种:
any(_Tp&& __value): _M_manager(&_Mgr::_S_manage){_Mgr::_S_create(_M_storage, std::forward<_Tp>(__value));}
当然根据是否应用了SSO, _S_create也分两种情况:
1. _Manager_internal 调用了placement new在原有内存上构造Tp:
template<typename _Up>static void_S_create(_Storage& __storage, _Up&& __value){void* __addr = &__storage._M_buffer;::new (__addr) _Tp(std::forward<_Up>(__value)); //调用copy ctor/move ctor}template<typename... _Args>static void_S_create(_Storage& __storage, _Args&&... __args){void* __addr = &__storage._M_buffer;::new (__addr) _Tp(std::forward<_Args>(__args)...); //调用parameterized ctor}
2. _Manager_external在堆上构造Tp
template<typename _Up>static void_S_create(_Storage& __storage, _Up&& __value){__storage._M_ptr = new _Tp(std::forward<_Up>(__value));}template<typename... _Args>static void_S_create(_Storage& __storage, _Args&&... __args){__storage._M_ptr = new _Tp(std::forward<_Args>(__args)...);}
OK,前三种初始化讲完了,来看下第四种:assignement operator
template<typename _Tp>enable_if_t<is_copy_constructible<_Decay_if_not_any<_Tp>>::value, any&>operator=(_Tp&& __rhs){*this = any(std::forward<_Tp>(__rhs));return *this;}
它先调用了第一种初始化方式构造了一个临时any对象,再调用any的mtor把数据move过去,move委托给了_Manager_**ternal::_S_manage
any(any&& __other) noexcept{if (!__other.has_value())_M_manager = nullptr;else{_Arg __arg;__arg._M_any = this;__other._M_manager(_Op_xfer, &__other, &__arg);}}
5. 原理-获取数据
数据的获取必须通过any_cast,其语法如下:
无论是哪种形式,都会调用到__any_caster:
template<typename _Tp>void* __any_caster(const any* __any){// any_cast<T> returns non-null if __any->type() == typeid(T) and// typeid(T) ignores cv-qualifiers so remove them:using _Up = remove_cv_t<_Tp>;// The contained value has a decayed type, so if decay_t<U> is not U,// then it's not possible to have a contained value of type U:if constexpr (!is_same_v<decay_t<_Up>, _Up>)return nullptr;// 又一次要求存储的类型必须拷贝可构造!else if constexpr (!is_copy_constructible_v<_Up>)return nullptr;// First try comparing function addresses, which works without RTTIelse if (__any->_M_manager == &any::_Manager<_Up>::_S_manage
#if __cpp_rtti|| __any->type() == typeid(_Tp)
#endif){return any::_Manager<_Up>::_S_access(__any->_M_storage); //真正的访问数据}return nullptr;}
当然根据是否用了SSO,依然有两种情况。代码比较简单,我就不解释了。
//_Manager_internal SSOstatic _Tp*_S_access(const _Storage& __storage){// The contained object is in __storage._M_bufferconst void* __addr = &__storage._M_buffer;return static_cast<_Tp*>(const_cast<void*>(__addr));}//_Manager_externalstatic _Tp*_S_access(const _Storage& __storage){// The contained object is in *__storage._M_ptrreturn static_cast<_Tp*>(__storage._M_ptr);}