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 type
std::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 cast
try
{
a = 1;
std::cout << std::any_cast<float>(a) << '\n';
}
catch (const std::bad_any_cast& e)
{
std::cout << e.what() << '\n';
}
// has value
a = 2;
if (a.has_value())
std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
// reset
a.reset();
if (!a.has_value())
std::cout << "no value\n";
// pointer to contained data
a = 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 ctor
operator=(_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 optimization
template<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>
void
any::_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 RTTI
else 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 SSO
static _Tp*
_S_access(const _Storage& __storage)
{
// The contained object is in __storage._M_buffer
const void* __addr = &__storage._M_buffer;
return static_cast<_Tp*>(const_cast<void*>(__addr));
}
//_Manager_external
static _Tp*
_S_access(const _Storage& __storage)
{
// The contained object is in *__storage._M_ptr
return static_cast<_Tp*>(__storage._M_ptr);
}