当前位置: 首页 > news >正文

跟我学C++中统篇—STL中的bind

一、标准库中的函数绑定

对C++11标准比较熟悉的都知道,标准库中提供了一个函数模板std::bind,用于将可调用对象(函数,仿函数、函数指针、lambda表达式及函数对象等)与一组参数绑定,然后形成一个新的可调用对象,即Callable Object。当后面调用这个可调用对象时,则可以根据实际情况进行相关的处理。std::bind支持对部分参数绑定、参数占位符、成员函数绑定和嵌套绑定,同时有着更好的兼容性。当然,缺点也有,就是有点“重”。
在C++20/23中,标准库中又提供了两个bind函数,std::bind_front和std::bind_back,顾名思义,可以知道是可以绑定可调用对象的前面或后面的N个参数,让绑定操作变得更灵活方便。下面对它们进行对比分析说明。

二、std::bind

其定义如下:

template<class F,class...Args>/* unspecified */bind(F&&f,Args&&...args);(1)(since C++11)(constexpr since C++20)template<class R,class F,class...Args>/* unspecified */bind(F&&f,Args&&...args);(2)(since C++11)(constexpr since C++20)

std::bind是最通用也是最早的一个绑定接口,它的功能最齐全但应用起来也相对复杂的多。一般在处理函数回调、事件控制、消息管理等需要提前进行映射的情况可以根据情况来主动的控制过滤相关的接口。它还可以实现类似partial application的应用,请参看前面相关的文章。
需要注意的是,要清楚占位符如何使用确保其映射的位置的正确性(参看后面的例程);在绑定类成员函数时,要处理好其生存周期,防止出现悬垂引用等异常;绑定中处理引用等需要显式的使用std::ref或std::cref进行控制;最后,需要开发者自行处理好重载的控制。
在C++14以后,其实更推荐开发者使用lambda表达式来替代std::bind(std::function)的应用。

三、std::bind_front和std::bind_back

std::bind_front和std::bind_back,可以认为是对std::bind在某些场景下的快速应用,主打一个简单方便。通过指定前或后几个参数的绑定控制,来实现接口过滤。由于其没有复杂的占位符,更容易为编译器优化。其定义如下:

std::bind_front template<class F,class...Args>constexpr/* unspecified */bind_front(F&&f,Args&&...args);(1)(since C++20)template<autoConstFn,class...Args>constexpr/* unspecified */bind_front(Args&&...args);(2)(since C++26)std::bind_back template<class F,class...Args>constexpr/* unspecified */bind_back(F&&f,Args&&...args);(3)(since C++23)template<autoConstFn,class...Args>constexpr/* unspecified */bind_back(Args&&...args);(4)(since C++26)

注意一下C++26中的用法,可以与下面的例程代码进行对照。C++26中的用法更让开发者容易与模板的开发匹配。
其可能的实现:

//std::bind_frontnamespace detail{//对常量的处理,如T为const,则U也增加const限定符(通过conditional判断后选择是否增加)template<class T,class U>structcopy_const:std::conditional<std::is_const_v<T>,Uconst,U>{};//删除引用//T&&是万能引用,通过引用折叠判断左值引用类型,不清楚可参看相关资料//通过conditional判断返回左值X&或右值 X&&template<class T,class U,class X=typename copy_const<std::remove_reference_t<T>,U>::type>structcopy_value_category:std::conditional<std::is_lvalue_reference_v<T&&>,X&,X&&>{};//删除U的引用后,将T的类型和CV限定符照搬到Utemplate<class T,class U>structtype_forward_like:copy_value_category<T,std::remove_reference_t<U>>{};//辅助模板template<class T,class U>usingtype_forward_like_t=typename type_forward_like<T,U>::type;}//bind_front是一个非类型模板并使用了变参template<autoConstFn,class...Args>constexprautobind_front(Args&&...args){using F=decltype(ConstFn);//指针类型判断ifconstexpr(std::is_pointer_v<F>or std::is_member_pointer_v<F>)static_assert(ConstFn!=nullptr);//返回deducing this(c++23)的lambdareturn[...bound_args(std::forward<Args>(args))]<class Self,class...T>//bound_args:C++20 的 init-capture pack ,将参数完美转发到bound_args...(//参数类型为std::decay_t<Args>this Self&&,T&&...call_args//获取自身的值类别,如左或右值及有无cv限定符)noexcept//异常处理(//参数匹配处理,前面分析过std::is_nothrow_invocable_v<F,detail::type_forward_like_t<Self,std::decay_t<Args>>...,T...>)//invoke_result_t推导保证SFINAE->std::invoke_result_t<F,detail::type_forward_like_t<Self,std::decay_t<Args>>...,T...>{//等同于 C++23中std::forward_like,以目标类型控制表达式的值类别(左值/右值)和 const限定符,实现智能的转发returnstd::invoke(ConstFn,std::forward_like<Self>(bound_args)...,std::forward<T>(call_args)...);};}//std::bind_back:分析可参看上面的bind_frontnamespace detail{/* is the same as above */}template<autoConstFn,class...Args>constexprautobind_back(Args&&...args){using F=decltype(ConstFn);ifconstexpr(std::is_pointer_v<F>or std::is_member_pointer_v<F>)static_assert(ConstFn!=nullptr);return[...bound_args(std::forward<Args>(args))]<class Self,class...T>(this Self&&,T&&...call_args)noexcept(std::is_nothrow_invocable_v<F,detail::type_forward_like_t<Self,T...,std::decay_t<Args>>...>)->std::invoke_result_t<F,detail::type_forward_like_t<Self,T...,std::decay_t<Args>>...>{returnstd::invoke(ConstFn,std::forward<T>(call_args)...,std::forward_like<Self>(bound_args)...);};}

这两个模板接口其实可以理解为bind-partial,偏绑定。分析看上面代码的注释。

四、三者的不同

这三个绑定的在不同的C++标准中推出,做偏绑定的后两者与bind的不同在于:

  1. 占位符的支持
    bind支持占位符,后面两个不支持。且由于占位符的支持与否,引出了bind支持参数的任意重排,而后面两个不支持
  2. 性能优势
    bind比较重,所以在性能上要比小后两者
  3. 简单性
    相对于bind使用的复杂,后二者则相对简单许多,特别是到C++26,更容易为开发者理解和应用
  4. 参数绑定支持
    bind支持任意参数的绑定,而std::bind_front只支持可调用对象参数列表前面的参数绑定支持,std::bind_back只支持可调用对象参数列表后面的参数绑定

五、例程

cppreference上的例程写得很全面简洁。

  1. std::bind的例程:
#include<functional>#include<iostream>#include<memory>#include<random>voidf(intn1,intn2,intn3,constint&n4,intn5){std::cout<<n1<<' '<<n2<<' '<<n3<<' '<<n4<<' '<<n5<<'\n';}intg(intn1){returnn1;}structFoo{voidprint_sum(intn1,intn2){std::cout<<n1+n2<<'\n';}intdata=10;};intmain(){using namespace std::placeholders;// for _1, _2, _3...std::cout<<"1) argument reordering and pass-by-reference: ";intn=7;// (_1 and _2 are from std::placeholders, and represent future// arguments that will be passed to f1)autof1=std::bind(f,_2,42,_1,std::cref(n),n);n=10;f1(1,2,1001);// 1 is bound by _1, 2 is bound by _2, 1001 is unused// makes a call to f(2, 42, 1, n, 7)std::cout<<"2) achieving the same effect using a lambda: ";n=7;autolambda=[&ncref=n,n](autoa,autob,auto/*unused*/){f(b,42,a,ncref,n);};n=10;lambda(1,2,1001);// same as a call to f1(1, 2, 1001)std::cout<<"3) nested bind subexpressions share the placeholders: ";autof2=std::bind(f,_3,std::bind(g,_3),_3,4,5);f2(10,11,12);// makes a call to f(12, g(12), 12, 4, 5);std::cout<<"4) bind a RNG with a distribution: ";std::default_random_engine e;std::uniform_int_distribution<>d(0,10);autornd=std::bind(d,e);// a copy of e is stored in rndfor(intn=0;n<10;++n)std::cout<<rnd()<<' ';std::cout<<'\n';std::cout<<"5) bind to a pointer to member function: ";Foo foo;autof3=std::bind(&Foo::print_sum,&foo,95,_1);f3(5);std::cout<<"6) bind to a mem_fn that is a pointer to member function: ";autoptr_to_print_sum=std::mem_fn(&Foo::print_sum);autof4=std::bind(ptr_to_print_sum,&foo,95,_1);f4(5);std::cout<<"7) bind to a pointer to data member: ";autof5=std::bind(&Foo::data,_1);std::cout<<f5(foo)<<'\n';std::cout<<"8) bind to a mem_fn that is a pointer to data member: ";autoptr_to_data=std::mem_fn(&Foo::data);autof6=std::bind(ptr_to_data,_1);std::cout<<f6(foo)<<'\n';std::cout<<"9) use smart pointers to call members of the referenced objects: ";std::cout<<f6(std::make_shared<Foo>(foo))<<' '<<f6(std::make_unique<Foo>(foo))<<'\n';}
  1. std::bind_front和std::bind_back例程
#include<cassert>#include<functional>intminus(inta,intb){returna-b;}structS{intval;intminus(intarg)constnoexcept{returnval-arg;}};intmain(){autofifty_minus=std::bind_front(minus,50);assert(fifty_minus(3)==47);// equivalent to: minus(50, 3) == 47automember_minus=std::bind_front(&S::minus,S{50});assert(member_minus(3)==47);//: S tmp{50}; tmp.minus(3) == 47// Noexcept-specification is preserved:static_assert(!noexcept(fifty_minus(3)));static_assert(noexcept(member_minus(3)));// Binding of a lambda:autoplus=[](inta,intb){returna+b;};autoforty_plus=std::bind_front(plus,40);assert(forty_plus(7)==47);// equivalent to: plus(40, 7) == 47#if__cpp_lib_bind_front>=202306LC++26中的用法,和上面的定义对照autofifty_minus_cpp26=std::bind_front<minus>(50);assert(fifty_minus_cpp26(3)==47);automember_minus_cpp26=std::bind_front<&S::minus>(S{50});assert(member_minus_cpp26(3)==47);autoforty_plus_cpp26=std::bind_front<plus>(40);assert(forty_plus(7)==47);#endif#if__cpp_lib_bind_back>=202202Lautomadd=[](inta,intb,intc){returna*b+c;};automul_plus_seven=std::bind_back(madd,7);assert(mul_plus_seven(4,10)==47);//: madd(4, 10, 7) == 47#endif#if__cpp_lib_bind_back>=202306Lautomul_plus_seven_cpp26=std::bind_back<madd>(7);assert(mul_plus_seven_cpp26(4,10)==47);#endif}

bind的操作应用本身并没有什么难度,看看代码就明白了。

六、总结

一般来说,如果环境支持高版本的C++标准并且应用不复杂,推荐使用std::bind_front和std::bind_back,如果再复杂可使用lambda表达式,尽量减少或避免std::bind的使用。只在老的标准代码兼容或为特定的复杂场景下再使用。
本文的重点不是对这几个bind模板接口的详细说明,毕竟出现时间已经很长了。主要是对新标准中的几种bind进行整体的综合分析,以便可以更好的对bind操作,特别与前面手动实现partial application有一个对照分析和理解。目标还是掌握“所以然”,以便在模板和元编程开发中能够更加游刃有余。

http://www.jsqmd.com/news/248381/

相关文章:

  • 【好写作AI】别吵了!用AI写的论文,到底算谁的?一个灵魂拷问的终极回答
  • 雷池WAF安装
  • 【好写作AI】毕业季“分身术”:用AI把一天48小时的魔法变成现实
  • 拥抱大模型:深入剖析 ReAct 的核心原理、技术架构及其对 AI 领域的深远影响
  • 5 款 AI 写论文哪个好?实测横评!虎贲等考 AI 凭硬核实力 C 位胜出
  • 【好写作AI】一次“氪金”,终身受益?这笔毕业季投资到底值不值?
  • 从单智能体到多智能体:九种模式教你搭建高效AI应用
  • 虎贲等考 AI:重塑学术写作新范式,全流程智能赋能论文创作
  • Java IO 与 NIO:从 BIO 阻塞陷阱到 NIO 万级并发
  • 从Windows 10上为远程Linux上安装claude code环境
  • 新一代开源CRM系统源码功能全览,支持设定年度、季度、月度等多层级业绩指标
  • 什么是 MCP?如何在 Spring Boot + LangChain4j 中落地实战?
  • 60N02-ASEMI藏在电路里的“效率密码”
  • 课程论文别再 “凑字数”!虎贲等考 AI:一键解锁高分学术答卷的秘密
  • 顶刊级科研绘图不用愁!虎贲等考 AI 一键解锁论文 “视觉加分密码”
  • 【Java毕设全套源码+文档】基于springboot的家校互联管理系统设计与实现(丰富项目+远程调试+讲解+定制)
  • 全面解析 Agent Engineering 的十大工程维度:生产级 Agent 系统的炼成之路
  • 【Java毕设全套源码+文档】基于JavaWeb的毕业季旅游一站式定制服务平台的设计与实现(丰富项目+远程调试+讲解+定制)
  • 为什么有了 LangChain,还需要 Spring AI?
  • 开题报告总被毙?虎贲等考 AI:三步搞定导师认可的学术蓝图
  • 人工磨问卷 VS 虎贲等考 AI:3 天工作量压缩到 30 分钟的调研革命
  • 【Java毕设全套源码+文档】基于Java Web的美容美发管理系统的设计与实现(丰富项目+远程调试+讲解+定制)
  • 数据不会说话?虎贲等考 AI 一键解锁 “学术数据翻译官” 模式
  • 【Java毕设全套源码+文档】基于springboot的房屋租赁管理系统设计与实现(丰富项目+远程调试+讲解+定制)
  • 计算机毕业设计Django+Vue.js新闻推荐系统 新闻可视化 大数据毕业设计(源码+LW文档+PPT+详细讲解)
  • 吐血推荐10个AI论文软件,本科生搞定毕业论文!
  • (9-2-03)自动驾驶中基于概率采样的路径规划:基于Gazebo仿真的路径规划系统(3)
  • 【Java毕设全套源码+文档】基于springboot的郑州旅游景点智能推荐系统设计与实现(丰富项目+远程调试+讲解+定制)
  • HMC349AMS8GETR,高线性度与高功率处理的射频开关
  • 在CentOS上快速安装NVM和Node.js 14:完整指南与优化方案