我的C++实践(12):函数指针与仿函数

    C++的函数调用语法实体有函数、类似于函数的宏、函数指针、仿函数(即函数对象)。函数调用的方式有:
    (1)直接调用:通过函数名直接调用函数,函数起始地址成为指令的一部分,因此编译期就能确定调用了哪个函数。
    (2)间接调用:通过函数指针来调用函数,函数起始地址位于内存或寄存器的某处,因此到运行期才能确定调用哪个函数。
    (3)内联调用:直接在调用处展开函数代码,在编译期进行,因此到运行期就不存在函数调用了。
    对函数指针和仿函数的使用,基本上都是某种形式的回调。通常的程序库是库作者来编写库的代码,让用户来使用这些库,而回调正好相反,程序库调用了某个函数,而这个函数的代码需要由使用库的用户来编写。在实际应用中,仿函数与函数指针相比有很多优点,因为仿函数是类对象,因此我们可以在仿函数中关联状态信息(当然这会使仿函数类占内存空间,但我们可以使用空基类优化技术),这样根据不同的参数生成不同的函数实例。仿函数可以作为模板的类型实参来传递,也可以作为函数调用实参来传递。但仿函数是类对象,因此不能作为非类型模板实参,但函数指针可以作为非模板模板实参。通过仿函数,我们可以把所有的的普通函数都封装成仿函数类,这样就可以以对象的方式来统一使用各个函数。我们也可以对仿函数的各个参数类型和返回类型进行萃取,从而使其具有反射的能力,不同的仿函数还可以方便地组合起来使用。
    编写仿函数类的基本约定: 在仿函数类中要定义仿函数的参数个数常量NumParams、每个参数的类型typedef Param1T,typedef Param2T,...、返回类型tyepdef ReturnT。根据约定,我们的仿函数类应该编写成类似于下面的形式:

    1、仿函数的类型萃取实现: FunctorParam<F,N>用来获取仿函数F的第N的参数的类型,当N值不大于实际的参数个数时,调用UsedFunctorParam<F,N>模板针对N的特化来获取相应位置处的参数类型,当N值大于实际的参数个数时,返回私有的无用嵌套成员类 NNest,它不能用来创建对象,因此没有副作用。代码如下:

    解释:
    (1)UsedFuctorParam<F,N>用来抽取仿函数F的实际使用的第N个参数类型,这里N没有超过仿函数的实际参数个数。参数类型的抽取是通过对每个N值提供一个局部特化来实现的。假设仿函数的参数个数最多为20,对具有工业强度的库而言,其设计一般要达到20个参数左右。实际中很少有多于20个参数的仿函数。这里#undef表示宏FunctorParamSpec的生命期结束,即下面就已经不存在FunctorParamSpec这个宏了,要用的话需要重新定义。
    (2)FunctorParam<F,N>中,N可以超过仿函数的实际参数个数,当超过时,就返回一个私有的无用嵌套类NNest,NNest在FunctorParam内部,在外部并不能直接用类名NNest来创建对象,因此对不存在的参数虽然返回了一个类型,但并没有产生副作用。这里用到了类型萃取技术中开发的IfThenElse模板。       
    2、把函数指针封装成仿函数: 直接对函数指针的返回类型和各个参数类型进行参数化,设计成仿函数类模板。
    下面的ForwardParamT<T>用于决定仿函数的参数类型T是传引用还是传原来的普通类型,T为类类型时则传引用,其他情况则传原来的普通类型(这需要用到类型萃取技术中开发的TypeOp模板),这样可以避免昂贵的拷贝构造函数调用。对仿函数参数为空(即为void)的情况,提供一个全局特化,指定空参数的类型为一个无副作用的哑类型。

    下面的FunctionPtrT用来根据函数指针的返回类型和各个参数类型构造出函数指针(这里针对最多为5个参数的情况,对含有更多个参数的函数原理和做法是一样的)。最后的FunctionPtr就是真正封装函数指针的仿函数类型,要遵循仿函数的编写约定。它组合一个用FunctionPtrT构造出来的函数指针,然后根据函数指针的不同实参个数(0-5个)重载operator(),并用函数指针来进行调用实际的函数即可。在调用函数时,用到了上面的ForwardParamT<T>,这样就可以在传递调用实参时避免拷贝构造函数的调用。包装函数func_ptr(*fp)用来方便地创建封装了函数指针的函数对象。

    测试代码如下:

    3、仿函数的组合: Composer<FO1,FO2>用来组合两个仿函数的使用,先调用仿函数FO1(其参数个数可以是0-20个),再把其返回的结果的用作实参来调用仿函数FO2。可见FO2只能有一个参数,且与FO1的返回类型相同,这样才能组合使用。包装函数compose(f1,f2)用来方便地创建两个仿函数的组合对象。

    解释:
    (1)这里让Composer<FO1,FO2>通过BaseMem间接继承模板参数FO1,FO2,以便当FO1和FO2为空类时可以进行空基类优化。如果Composer直接继承FO1和FO2,当FO1和FO2为同一个类型时,编译会出错,因此我们需要要引入间接层BaseMem,现在的基类BaseMem<FO1,1>和BaseMem<FO2,2>并不是同一个类型。
    (2)Composer是仿函数类,需要遵循仿函数的编写约定。它的各个参数类型是FO1的各个参数类型。因此我们需要用FunctorParam来抽取仿函数FO1的各个参数类型,以作为Composer的各个参数Param1T, Param2T,...,Param20T。
    (3)构造函数用于组合两个仿函数对象,它需要考虑两个仿函数的const和非const的情况。下面的函数调用运算符中是先调用FO1,把返回结果作为实参再调用FO2。
    4、仿函数参数的值绑定: 即将仿函数的某个参数绑定为一个特定的值,这样仿函数的参数就可以减少一个。我们需要开发出一个绑定器(是一个仿函数)来作绑定操作,一个独立的类来存放绑定值。绑定器的各个参数类型萃取也比较麻烦,它需要去掉仿函数中被绑定的参数,然后后面的参数又要前移一个位置,因此我们把绑定器的参数类型萃取抽离出来设计成一个独立的模板。   
    下面的BoundVal<T>用于在运行期存放T类型的绑定值(这个值后面会被绑定到仿函数的某个参数上去),而StaticBoundVal<T> 则用于在编译期存放T类型的绑定值。

    下面的BinderParams<F,P>用来萃取绑定器的参数类型。它只要去掉仿函数中被绑定的参数, 并把后面的参数前移1个位置即可。

 

    下面的SignSelectT<S,NegT,ZeroT,PosT>根据整数S是正、0、还是负来选择相应的类型。

    下面是真正的绑定器Binder<FO,P,V>。它把仿函数FO的第P个参数绑定为一个存放在V类型对象中的绑定值,绑定后它还要完成对仿函数FO的调用,即Binder<FO,P,V>本身就相当于被绑定后的FO。Binder的参数类型列表Params是仿函数FO去掉绑定参数后的各个参数类型,可见Binder::operator()的参数个数比底层的FO::operator()少一个,因此它在调用底层的FO::operator()时需要重新确定原来FO的参数,并指定一个参数为绑定值。嵌套类模板ArgSelect<A>就是用来确定FO的各个实参。包装函数bind<P>(fo,val)用于方便地将仿函数对象fo的第P个参数绑定为值val。

    解释:
    (1)Binder继承自模板参数FO和V,这样就可以使用空基类优化技术。Binder是一个仿函数,因此它要遵循仿函数的编写约定,定义参数个数和各个参数类型(用上面的BinderParams来萃取)。
    (2)构造函数根据传过来的仿函数对象和存放了绑定值的V类型对象构造Binder,它需要考虑各种const和非const的情况。而构造函数模板则直接根据仿函数对象和要绑定的值(T类型的)构造Binder,绑定的值存储在BoundVal<T>中,即在运行期存储绑定值。
    (3)ArgSelect<A>用来确定原来FO的各个实参,这样才能在Binder中完成对被绑定的FO的调用。它先根据A的位置定义绑定实参前面的类型NoSkipT、绑定实参后面的类型SkipT、以及绑定实参处的类型BindT。然后定义3个不同的嵌套类,里面有一个静态的select函数,用来实现3种情况下对FO实参值的选择。接着根据A与P的相对位置,用SignSelectT得到仿函数FO中A处的参数类型、以及仿函数FO的实参选择类(即上面的的3个嵌套类之一)。最后是真正的实参选择函数from,它调用相应选择类的select,根据实参位置A与P的相对位置,自动选择FO中A处是使用用户提供的实参,还是使用被绑定的值。
    (4)函数调用Binder::operator()对调用实参个数不同的情况,都要进行重载。Binder::operator()直接调用FO::operator(),而各个实参是根据所在位置用ArgSelect<n>::from来自动选择,对FO中绑定处的参数用绑值V::get(),对其他位置的参数则用用户传递过来的实参。
    (5)包装函数bind<P>(fo,val)直接返回一个对fo绑定后的少了一个参数的仿函数对象。它的使用就相当于对少了一个参数的fo的使用。
    测试代码如下:

    5、普通函数参数的值绑定: 可先用上面的func_ptr(*fp)把函数(最多只能接受5个参数)封装成仿函数,再用bind来进行值绑定即可。上面的测试代码中就是这样用的。从中我们可以看出,通过使用func_ptr和bind的实现,我们可以对任意普通函数的参数进行值绑定。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值