C++:std::function的libc++实现

std::function是个有点神奇的模板,无论是普通函数、函数对象、lambda表达式还是std::bind的返回值(以上统称为可调用对象(Callable)),无论可调用对象的实际类型是什么,无论是有状态的还是无状态的,只要它们有相同参数类型和返回值类型,就可以使用同一类型的std::function进行存储和调用。这种特性被称作类型擦除(Type erasure),它允许我们在不知道对象实际类型的情况下对对象进行存储和操作。

在本文中,我将以std::functionlibc++实现(14.0版本)为例,分析std::function类型擦除的实现原理,以及实现一个精简版的std::functionMyFunction

std::function如何实现类型擦除?

在不知道对象实际类型的情况下操作对象,有一种常规的手段可以实现这个功能,那就是多态,libc++版的std::function正是基于虚函数实现的。具体是如何实现的呢?我们可以从考察std::function在被调用时发生了什么作为这个问题的切入点。

对于以下代码:

#include <functional>
#include <iostream>
int main() {std::function<void()> f = []() {  //std::cout << "Hello, world!" << std::endl;};f();return 0;
}

std::cout一行打断点,运行,得到以下堆栈:

#0  main::$_0::operator() (this=0x7fffffffdb18) at /mnt/d/code/function_test/call.cpp:6
#1  0x0000555555557745 in std::__1::__invoke<main::$_0&> (__f=...) at /usr/lib/llvm-14/bin/../include/c++/v1/type_traits:3640
#2  0x00005555555576fd in std::__1::__invoke_void_return_wrapper<void, true>::__call<main::$_0&> (__args=...) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/invoke.h:61
#3  0x00005555555576cd in std::__1::__function::__alloc_func<main::$_0, std::__1::allocator<main::$_0>, void ()>::operator()() (this=0x7fffffffdb18) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:180
#4  0x0000555555556839 in std::__1::__function::__func<main::$_0, std::__1::allocator<main::$_0>, void ()>::operator()() (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:354
#5  0x0000555555558622 in std::__1::__function::__value_func<void ()>::operator()() const (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:507
#6  0x00005555555577d5 in std::__1::function<void ()>::operator()() const (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:1184
#7  0x00005555555562e5 in main () at /mnt/d/code/function_test/call.cpp:8

不考虑lambda本身,以及invoke相关的类,std::function实现相关的类有以下几个:

  1. std::__1::function<void ()>
  2. std::__1::__function::__value_func<void ()>
  3. std::__1::__function::__func<main::$_0, std::__1::allocator<main::$_0>, void ()>
  4. std::__1::__function::__alloc_func<main::$_0, std::__1::allocator<main::$_0>, void ()>

lambda的类型被定义为了main::$_0,可以看出来,function__function::__value_func两个模板类不依赖lambda实际类型,__function::__func__function::__alloc_func对lambda类型有依赖。

std::function

std::function看起,被声明为拥有一个模板参数_Fp。我们使用的是它的特化版本,具有两个模板参数,返回值类型_Rp和参数列表类型_ArgTypes(接下来几个类也都是特化出来的,不再赘述)。它有一个__function::__value_func<_Rp(_ArgTypes...)>类型的成员__f_

template<class _Fp> class function;template<class _Rp, class ..._ArgTypes>
class function<_Rp(_ArgTypes...)>
{typedef __function::__value_func<_Rp(_ArgTypes...)> __func;__func __f_;...
};
...
template <class _Rp, class... _ArgTypes>
template <class _Fp, class>
function<_Rp(_ArgTypes...)>::function(_Fp __f) : __f_(_VSTD::move(__f)) {}

std::functionoperator()被调用时,它只是地把调用转发给__f_

template<class _Rp, class ..._ArgTypes>
_Rp
function<_Rp(_ArgTypes...)>::operator()(_ArgTypes... __arg) const
{return __f_(_VSTD::forward<_ArgTypes>(__arg)...);
}

__function::__value_func

看看__function::__value_func具体是什么类型:

// __value_func creates a value-type from a __func.
template <class _Fp> class __value_func;template <class _Rp, class... _ArgTypes> class __value_func<_Rp(_ArgTypes...)>
{typename aligned_storage<3 * sizeof(void*)>::type __buf_;typedef __base<_Rp(_ArgTypes...)> __func;__func* __f_;...
};

它的模板参数和std::function一致,有两个成员,一个成员是有3个指针大小的__buf_,另一个成员是__function::__base<_Rp(_ArgTypes...)>*类型的__f_

__function::__value_func的构造函数相对复杂一些,主要是为了做一个优化:当__f_指向的对象的大小小于等于__buf_的大小,也就是3个指针时,__f_会被构造在__buf_上,这样可以减少堆上内存的分配:

template <class _Fp, class _Alloc>
__value_func(_Fp&& __f, const _Alloc& __a): __f_(nullptr)
{typedef allocator_traits<_Alloc> __alloc_traits;typedef __function::__func<_Fp, _Alloc, _Rp(_ArgTypes...)> _Fun;typedef typename __rebind_alloc_helper<__alloc_traits, _Fun>::type_FunAlloc;if (__function::__not_null(__f)){_FunAlloc __af(__a);if (sizeof(_Fun) <= sizeof(__buf_) &&is_nothrow_copy_constructible<_Fp>::value &&is_nothrow_copy_constructible<_FunAlloc>::value){__f_ =::new ((void*)&__buf_) _Fun(_VSTD::move(__f), _Alloc(__af));}else{typedef __allocator_destructor<_FunAlloc> _Dp;unique_ptr<__func, _Dp> __hold(__af.allocate(1), _Dp(__af, 1));::new ((void*)__hold.get()) _Fun(_VSTD::move(__f), _Alloc(__a));__f_ = __hold.release();}}
}

需要注意到的一个细节是:__f_在模板类定义中的类型是__function::__base,而此处new出来的对象类型是__function::__func,不难猜到,__function::__func继承了__function::__base

__function::__value_funcoperator()被调用时,它也只是在做完合法性检查后把调用转发给了*__f_

_Rp operator()(_ArgTypes&&... __args) const
{if (__f_ == nullptr)__throw_bad_function_call();return (*__f_)(_VSTD::forward<_ArgTypes>(__args)...);
}

__function::__base

下面是__function::__base,它是一个抽象模板类,模板参数和std::function一致,不包含可调用对象的具体类型:

template<class _Fp> class __base;template<class _Rp, class ..._ArgTypes>
class __base<_Rp(_ArgTypes...)>
{__base(const __base&);__base& operator=(const __base&);
public:_LIBCPP_INLINE_VISIBILITY __base() {}_LIBCPP_INLINE_VISIBILITY virtual ~__base() {}virtual __base* __clone() const = 0;virtual void __clone(__base*) const = 0;virtual void destroy() _NOEXCEPT = 0;virtual void destroy_deallocate() _NOEXCEPT = 0;virtual _Rp operator()(_ArgTypes&& ...) = 0;
#ifndef _LIBCPP_NO_RTTIvirtual const void* target(const type_info&) const _NOEXCEPT = 0;virtual const std::type_info& target_type() const _NOEXCEPT = 0;
#endif // _LIBCPP_NO_RTTI
};

__function::__func

然后是__function::__func,它继承了__function::__base,并且其模板参数含有可调用对象的类型_Fp,这正是实现类型擦除的关键:类型_Fp被隐藏了在了__function::__base这个抽象类后面。__function::__func含有一个类型为__function::__alloc_func的成员__f_

// __func implements __base for a given functor type.
template<class _FD, class _Alloc, class _FB> class __func;template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
class __func<_Fp, _Alloc, _Rp(_ArgTypes...)>: public  __base<_Rp(_ArgTypes...)>
{__alloc_func<_Fp, _Alloc, _Rp(_ArgTypes...)> __f_;
public:explicit __func(_Fp&& __f): __f_(_VSTD::move(__f)) {}...
};

__function::__funcoperator()依然只是转发调用:

template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
_Rp
__func<_Fp, _Alloc, _Rp(_ArgTypes...)>::operator()(_ArgTypes&& ... __arg)
{return __f_(_VSTD::forward<_ArgTypes>(__arg)...);
}

__function::__alloc_func

然后是最后一个类__function::__alloc_func,它有一个pair类型的成员__f_std::function构造时传入的可调用对象最终会存储在__f_中:

// __alloc_func holds a functor and an allocator.
template <class _Fp, class _Ap, class _FB> class __alloc_func;template <class _Fp, class _Ap, class _Rp, class... _ArgTypes>
class __alloc_func<_Fp, _Ap, _Rp(_ArgTypes...)>
{__compressed_pair<_Fp, _Ap> __f_;public:...explicit __alloc_func(_Target&& __f): __f_(piecewise_construct, _VSTD::forward_as_tuple(_VSTD::move(__f)),_VSTD::forward_as_tuple()){}...
};

__function::__alloc_funcoperator()方法中,调用转发给了__invoke_void_return_wrapper::__call,后面的流程就和std::function的实现无关了。

_Rp operator()(_ArgTypes&&... __arg)
{typedef __invoke_void_return_wrapper<_Rp> _Invoker;return _Invoker::__call(__f_.first(),_VSTD::forward<_ArgTypes>(__arg)...);
}

最终我们发现,“神奇”的类型擦除还是通过“朴素”的多态来实现的,之所以显得神奇是因为多态被隐藏了起来,没有暴露给用户。

std::function对构造参数的校验

仔细观察一下std::function的构造函数:

template <class _Rp, class... _ArgTypes>
template <class _Fp, class>
function<_Rp(_ArgTypes...)>::function(_Fp __f) : __f_(_VSTD::move(__f)) {}

构造函数对参数__f似乎并没有施加任何约束,如何真是那样,那我们在使用一个不恰当的_Fp类型构造std::function时,很可能会得到可读性极差的编译错误信息,因为std::function类本身对_Fp没有施加约束,那么实例化std::function时也就不太可能出现错误了,很有可能到了实例化__function::__alloc_func时编译错误才会报告出来,这是一个内部类,一般用户看到了关于它的实例化失败的错误信息大概会感到摸不着头脑。

但实际情况并不是这样的,假设你这样定义一个std::function对象:

std::function<void()> f(1);

你会得到一个比较清晰的编译错误信息:

/mnt/d/code/function_test/myfunction.cpp:107:27: error: no matching constructor for initialization of 'std::function<void ()>'std::function<void()> f(1);
...
/usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:998:5: note: candidate template ignored: requirement '__callable<int &, false>::value' was not satisfied [with _Fp = int]function(_Fp);
...

这是怎么做到的呢?答案藏在构造函数声明的第二个模板参数class = _EnableIfLValueCallable<_Fp>

template<class _Fp, class = _EnableIfLValueCallable<_Fp>>
function(_Fp);

此处使用了SFINAE技术,我们看看_EnableIfLValueCallable具体是怎么实现的:

template <class _Fp, bool = _And<_IsNotSame<__uncvref_t<_Fp>, function>,__invokable<_Fp, _ArgTypes...>
>::value>
struct __callable;
template <class _Fp>struct __callable<_Fp, true>{static const bool value = is_void<_Rp>::value ||__is_core_convertible<typename __invoke_of<_Fp, _ArgTypes...>::type,_Rp>::value;};
template <class _Fp>struct __callable<_Fp, false>{static const bool value = false;};template <class _Fp>
using _EnableIfLValueCallable = typename enable_if<__callable<_Fp&>::value>::type;

_EnableIfLValueCallable的实现依赖于__callable__callable是一个模板类,拥有两个模板参数,第一个模板参数_Fp是可调用对象的类型,第二个模板参数是bool类型的,当_IsNotSame<__uncvref_t<_Fp>, function>__invokable<_Fp, _ArgTypes...>这两个条件同时满足时,该模板参数为true,否则为false。

_IsNotSame<__uncvref_t<_Fp>, function>,顾名思义,是用来判断两个模板参数是否为同一类型的,这个条件似乎是为了避免歧义:当我们用另一个std::function构造std::function时,应该匹配到拷贝构造函数,而不是这个。

__invokable<_Fp, _ArgTypes...>则是用来判断_Fp是否接受传入_ArgTypes参数调用。

__callable第二个模板参数为false的特化中,将value直接定义为false。而模板参数为true的特化中,还添加了新的判断条件,用来校验可调用对象返回值的可转换性。

第一个条件为is_void<_Rp>::value,用来判断_Rpvoid类型。这意味着,即使可调用对象实际上有返回类型,但是std::function被定义为返回void,那么编译也是可以通过的。

第二个条件是__is_core_convertible<typename __invoke_of<_Fp, _ArgTypes...>::type, _Rp>::value,用来判断_Fp被调用后返回值可转换为_Rp

综上,_Fp要满足以下条件,std::function的构造函数才能正常实例化:

_Fp不是std::function && _Fp可以以_ArgTypes为参数调用 && (_Rpvoid || _Fp返回值类型可转换为_Rp)

这保证了当以不恰当的可调用对象构造std::function时,能够尽可能提前触发编译错误,提升编译错误信息的可读性。

MyFunction的实现

下面我们下面模仿libc++,实现一个“青春版”的std::functionMyFunction,它忽略掉了大部分细节,只实现了构造和调用部分的代码。

#include <functional>
#include <iostream>
#include <utility>template <typename Func>
class FunctionBase;template <typename Ret, typename... Args>
class FunctionBase<Ret(Args...)> {public:virtual Ret operator()(Args&&... args) = 0;
};template <typename Callable, typename Func>
class FunctionImpl;template <typename Callable, typename Ret, typename... Args>
class FunctionImpl<Callable, Ret(Args...)> : public FunctionBase<Ret(Args...)> {Callable c_;public:FunctionImpl(Callable&& c) : c_(std::move(c)) {}Ret operator()(Args&&... args) override {return std::invoke(c_, std::forward<Args>(args)...);}
};template <typename Func>
class MyFunction;template <typename Ret, typename... Args>
class MyFunction<Ret(Args...)> {FunctionBase<Ret(Args...)>* f_ = nullptr;public:template <typename Callable>MyFunction(Callable c) {f_ = new FunctionImpl<Callable, Ret(Args...)>(std::move(c));}Ret operator()(Args&&... args) {if (f_ == nullptr) {throw std::bad_function_call();}return (*f_)(std::forward<Args>(args)...);}
};void normalFunction() { std::cout << "I'm a normal function" << std::endl; }struct FunctionObject {void operator()() { std::cout << "I'm a function object" << std::endl; }
};int main() {MyFunction<void()> f0 = []() { std::cout << "I'm a lambda" << std::endl; };f0();MyFunction<void()> f1 = normalFunction;f1();MyFunction<void()> f2 = FunctionObject();f2();return 0;
}

结语

在没有std::function可用的年代或者场合,我们一般会选择使用函数指针来实现类似std::function的功能。在使用C实现的Linux内核代码中,我们仍可以看到大量的函数指针的存在,主要是用来实现回调函数。

相较函数指针,std::function最明显的优势在于可以方便地存储带状态的函数,而函数指针只能以比较丑陋的方式来实现这个特性。

其次是灵活性,std::function给客户代码施加的约束较小,我们可以使用任意形式的可调用对象:普通函数,lambda表达式,函数对象等,函数指针就没有这种灵活性了。

不过由于虚函数的存在,std::function多了一点性能开销,但这点开销对大多数常规应用来说都是微不足道的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/369408.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(一)Docker基本介绍

部署项目的发展 传统部署适合需要最大性能和可靠性的场景&#xff0c;但在资源利用和管理方面有显著劣势。虚拟化部署提供了良好的资源利用率和隔离性&#xff0c;适用于需要灵活扩展和多租户环境的场景&#xff0c;但存在性能开销。容器部署在轻量级、可移植性和资源利用率方面…

[SAP ABAP] 子例程

子例程 示例1 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 输出结果如下所示 示例2 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 输出结果如下所示 补充扩展练习 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 输出结果如下所示 提示…

深入理解【 String类】

目录 1、String类的重要性 2、常用方法 2、1 字符串构造 2、2 String对象的比较 2、3 字符串查找 2、4字符转换 数值和字符串转换&#xff1a; 大小写转化&#xff1a; 字符串转数组&#xff1a; 格式转化&#xff1a; 2、5 字符串替换 2、6字符串拆分 2、7 字符串…

vue项目创建+eslint+Prettier+git提交规范(commitizen+hooks+husk)

# 步骤 1、使用 vue-cli 创建项目 这一小节我们需要创建一个 vue3 的项目&#xff0c;而创建项目的方式依然是通过 vue-cli 进行创建。 不过这里有一点大家需要注意&#xff0c;因为我们需要使用最新的模板&#xff0c;所以请保证你的 vue-cli 的版本在 4.5.13 以上&#xff…

ELK日志系统和Filebeat采集器的学习总结

ELK是ElasticSerach、Logstash、Kina Logstash负责采集数据&#xff0c;Logstash有三个插件&#xff0c;input、filter、output&#xff0c;filter插件作用是对采集的数据进行处理&#xff0c;过滤的&#xff0c;因此filter插件可以选&#xff0c;可以不用配置。 ElasticSear…

Android super.img结构及解包和重新组包

Android super.img结构及解包和重新组包 从Android10版本开始&#xff0c;Android系统使用动态分区&#xff0c;system、vendor、 odm等都包含在super.img里面&#xff0c;编译后的最终镜像不再有这些单独的 image&#xff0c;取而代之的是一个总的 super.img. 1. 基础知识 …

npm安装依赖报错——npm ERR gyp verb cli的解决方法

1. 问题描述 1.1 npm安装依赖报错——npm ERR! gyp verb cli npm MARN deprecated axiosQ0.18.1: critical security vuLnerability fixed in v0.21.1. For more information, npm WARN deprecated svg001.3.2: This SVGO version is no Longer supported. upgrade to v2.x.x …

第二节:如何使用thymeleaf渲染html(自学Spring boot 3.x的第一天)

大家好&#xff0c;我是网创有方&#xff0c;今天来学习如何使用thymeleaf渲染html。该模板运用不广泛&#xff0c;所以本节内容了解既可。 第一步&#xff1a;创建html文件。 在模板templates目录下创建一个html文件。 编写代码如下&#xff1a; <!DOCTYPE html> <…

雷电模拟器报错remount of the / superblock failed: Permission denied remount failed

报错截图 解决方法 打开设置 设置配置system.vmdk可写入 解决

【MySQL】mysql访问

mysql访问 1.引入MySQL 客户端库2.C/C 进行增删改3.查询的处理细节4.图形化界面访问数据库4.1下载MYSQL Workbench4.2MYSQL Workbench远程连接数据库 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&a…

在Apache HTTP服务器上配置 TLS加密

安装mod_ssl软件包 [rootlocalhost conf.d]# dnf install mod_ssl -y此时查看监听端口多了一个443端口 自己构造证书 [rootlocalhost conf.d]# cd /etc/pki/tls/certs/ [rootlocalhost certs]# openssl genrsa > jiami.key [rootlocalhost certs]# openssl req -utf8 -n…

【C++】哈希表 ---开散列版本的实现

你很自由 充满了无限可能 这是很棒的事 我衷心祈祷你可以相信自己 无悔地燃烧自己的人生 -- 东野圭吾 《解忧杂货店》 开散列版本的实现 1 前言2 开散列版本的实现2.1 节点设计2.2 框架搭建2.3 插入函数2.4 删除函数2.5 查找操作2.6 测试 Thanks♪(&#xff65;ω&#x…

也说字母U:房子到底是什么?

​ 不记得是第几期了&#xff0c;湖南卫视有档很火的音乐节目叫《歌手》&#xff0c;那一期是最终是韩磊夺得了冠军&#xff0c;他有一杀手锏&#xff0c;叫《向天再借五百年》&#xff0c;他要不夺冠&#xff0c;好像大家也对不起对这首歌的印象&#xff0c;因为他是多少人的记…

创建本地仓库

一、新建挂载目录 二、将挂载本地镜像挂载到目录 三、配置yum仓库 一、新建挂载目录 mkdir /BenDiCangKu 二、将挂载本地镜像挂载到目录 1、先连接本地光盘 2、挂载光盘 mount /dev/sr0 /BenDiCangKu 3、查看挂载 由此可见挂载成功 三、配置yum仓库 1、新建yum仓库文件…

HumbleBundle7月虚幻捆绑包30件军事题材美术模型沙漠自然环境大逃杀模块化建筑可定制武器包二战现代坦克飞机道具丧尸士兵角色模型20240705

HumbleBundle7月虚幻捆绑包30件军事题材美术模型沙漠自然环境大逃杀模块化建筑可定制武器包二战现代坦克飞机道具丧尸士兵角色模型202407051607 这次HumbleBundle捆绑包是UE虚幻军事题材的&#xff0c;内容非常多。 有军事基地、赛博朋克街区、灌木丛景观环境等 HB捆绑包虚幻…

相关技术 太阳能热水器循环水泵制作技术

网盘 https://pan.baidu.com/s/1oAKwUEGkKnEgxE-F4znKow?pwdidxd 双温区蓄能供热型太阳能热水系统及其工作方法.pdf 双罐叠压节能恒温型太阳能热水机组.pdf 基于傅科电流的循环式风能热水器.pdf 基于太阳能利用的建筑冷热电联产系统及工作方法.pdf 基于太阳能和热泵的双蓄式热…

护航端侧大模型平稳健康发展,百度大模型内容安全Lite版正式发布

6月28日&#xff0c;WAVE SUMMIT深度学习开发者大会 2024 “智变应用、码动产业”平行论坛在北京召开。与会&#xff0c;百度大模型内容安全Lite版正式发布&#xff0c;可面向低算力和超低算力的终端大模型提供离线场景下的一站式安全解决方案&#xff0c;为各类终端大模型平稳…

AI大模型对话(上下文)缓存能力

互联网应用中&#xff0c;为了提高数据获取的即时性&#xff0c;产生了各种分布式缓存组件&#xff0c;比如Redis、Memcached等等。 大模型时代&#xff0c;除非是免费模型&#xff0c;否则每次对话都会花费金钱来进行对话&#xff0c;对话是不是也可以参照缓存的做法来提高命…

QT C++(QT控件 QLabel,QLCDNumber,QProgressBar,QCalendarWidget)

文章目录 1. QLabel2. QLCDNumber3. QProgressBar4. QCalendarWidget 1. QLabel QLabel常见属性&#xff1a; text&#xff1a;QLabel中的文本textFormat&#xff1a;文本中的格式 Qt::PlainText:纯文本Qt::RichText:富文本&#xff0c;支持html标签Qt::MarkdownText markdow…

文件打开的系统错误分析流程

当用户出现“Open file failed”错误时&#xff0c;手动产生dump文件。 &#xff08;1&#xff09;打开资源管理器&#xff0c;选择AppNameXXX.exe进程&#xff0c;右击鼠标选择“创建转储文件” (2) 生成转储文件 3.获取用户转储文件 4.用Visual studio2015打开dump文件分析…