1. function
本文基于 GCC 9.4
function
的作用就是将各种仿函数的调用统一起来;
1.1 类中非静态成员函数指针为什么是16字节
auto cptr = &A::myfunc; 类中非静态成员函数 ,其类型为 void (A::*)(int)
auto rptr = print_num; 普通函数
对应汇编代码如下所示,可以看出,编译器为 this
指针预留了 8 字节的空间,此时没有绑定 this
指针,因此赋值为 0;
mov QWORD PTR [rbp-32], OFFSET FLAT:A::myfunc(int)
mov QWORD PTR [rbp-24], 0
mov QWORD PTR [rbp-8], OFFSET FLAT:print_num(int, int)
此处也解释了为什么非静态成员函数无法作为 sort
等函数传入的仿函数指针;
1.2 存放对象
为简洁起见,以下代码中均删除了 const 对象或方法;
(1)function
中可以存放 lambda 表达式、重载()的结构体/类、函数指针、类中成员函数等对象,可以存储这些对象的指针,这些对象的调用方式是相似的;
函数指针调用方式 (*func_ptr)(arg);
重载()的结构体/类调用方式 (*object)(arg);
但这些对象的类型是不同的,如何存储这些对象是首要问题,如下 _Nocopy_types
所示,这里采用了 union 的设计方式,并且采用非常技巧的方式;
union _Nocopy_types
{
void* _M_object; 存放对象指针,例如 lambda 表达式、重载()的结构体/类
void (*_M_function_pointer)(); 存放函数指针
void (_Undefined_class::*_M_member_pointer)(); 指向 类中的成员函数 的指针,注意其大小为 16 字节(静态成员函数指针仍为 8 字节)
};
union [[gnu::may_alias]] _Any_data
{
void* _M_access() { return &_M_pod_data[0]; } 特例化版本,存取union实际指针,可直接为 placement new 提供指针位置
template<typename _Tp> 函数模板版本,调用上述版本并进行必须的类型转换,可为 new 服务
_Tp& 注意:这里以引用形式返回
_M_access()
{ return *static_cast<_Tp*>(_M_access()); }
_Nocopy_types _M_unused;
char _M_pod_data[sizeof(_Nocopy_types)]; 为存放函数指针设置,便于获取union地址
};
1.3 类之间的关系
_Functor
为模板参数
(1)在 _Base_manager 中
__stored_locally
为 Bool 值,用于判断是否需要在堆上存储,(一般是查看 _Functor
大小是否大于 16,因为其可能是一个类或 bind
)
若需要,则调用 new _Functor()
,并将对象指针存放到 _Any_data
中;(这也是上图为什么使用虚线的原因)
否则,直接存放到 _Any_data
中
(2)初始化过程
根据 __stored_locally
调用对应 _M_init_functor
调用 _M_access
存放对象
(3)调用过程
_M_invoker(_M_functor, std::forward<_ArgTypes>(__args)...);
在 _M_invoker
中,将上述过程转换为
此处 __functor 即为类中的 _M_functor
(*_M_get_pointer(__functor))(std::forward<_ArgTypes>(__args)...);
_M_get_pointer
函数就是获得实际对象指针,从而实现函数调用;
1.4 一些问题思考
(1)在 _M_get_pointer
中局部存储的情况为什么单独处理,为什么不能直接转换为_Functor*
if _GLIBCXX17_CONSTEXPR (__stored_locally) 第一种方式
{
const _Functor& __f = __source._M_access<_Functor>(); 获取对象本身
return const_cast<_Functor*>(std::__addressof(__f));
}
else // have stored a pointer
return __source._M_access<_Functor*>(); 第二种方式
这里的最开始猜测是,若是分配在堆区,那么局部存储的肯定是指针;
而若是局部存储,局部存储的可能是一个对象(例如,一个结构体),
std::function<void(int)> f_display = print_num; 存储函数指针
std::function<void(int)> f_display_obj = PrintNum(); 存储结构体
(2)进一步调试
如果存储的是对象,我们想要的其实是 &_M_pod_data[0]
,之后对其解引用就可以获得实际对象,
如果存储的堆区指针,则&_M_pod_data[0]
中存储的为堆区指针,我们需要的是*&_M_pod_data[0]
;
因此第一种方式获取的是, &_M_pod_data[0]
,适用于对象;
第二种方式获取的是,*&_M_pod_data[0]
,适用于堆区指针;
_Functor*& _M_access(){
return *static_cast<_Functor**>(_M_access());
}
auto tmp = static_cast<_Tp*>(myaccess()); 此步的转换获取了指向对象的指针
_Tp tmp1 = (*tmp); 取对象,进行转换,此步相当于获取对象中存储的内容,因此是错误的;
(3)之前一直很疑惑,为什么局部存储普通函数指针的时候,也要使用第一种方式?
之前感觉这种存储方式,最后需要二重取引用,怎么也与当前一重取引用对不上;
后来发现函数指针的调用有两种方式
void*(*rig)();
rig = myaccess;
(*rig)();
rig(); 这种方式与上述等价
而且从汇编代码来看,这两种方式生成的汇编代码都是一样的
(4)为何此处使用addressof
而不用取地址 & 符号
重载 & 描述符后,取出的地址与 this 指针不一定一致
可参考 https://en.cppreference.com/w/cpp/memory/addressof 中的示例;
1.5 总结
由上述分析来看,本质上来讲,gcc版本的实现中std::function
就是一个 固定大小的 字符数组,若该字符数组能够存放对象,则将其存放到此处,否则,在堆区创建对象,在此处存放对象指针;