5.1 简述泛化仿函数generalized functor(或叫generalized function object)
泛化仿函数(generalized functors)是STL八大组件之一,是一种“能以一般函数调用语法来调用”的类。对于泛化仿函数的定义是比较严密的:能与拥有operator () 成员函数的对象有相同的调用形态。这个定义包括了普通的函数指针在内也算仿函数,但是之所以不直接强调所有函数指针的原因是非静态成员函数(nonstatic member function)不属于泛化仿函数的范围。
STL提供了很多仿函数以供使用,仿函数的使用多会配合算法(algorithm),STL中基本每个算法都提供了一个接受仿函数参数的重载版本,也有某些算法无法根据参数个数进行重载(参数类型区别无法重载,因为所有参数类型都被定义成了模板参数)的都在原算法名上加了”if”作为标示,例如find_if()就是find()接收仿函数作为判断条件版本。但是如果你想在STL的算法中用到你自己的仿函数就必须了解STL算法中调用仿函数的方式才能写出能与其配合的仿函数,这似乎也给广大程序员们带来多少的不方便,毕竟用到STL的程序员都应该阅读过STL源代码。但是如果我们使用的不是STL库呢?
仿函数其实不光是在STL中常用到,在其他库中也会常常见到,这是因为使用仿函数能通过统一的调用方式“operator ()”降低对象之间的联系(耦合性)。为了能让我们更好的使用仿函数,我打算将其泛化。(思路参考【Andrei 2001】 & STL::functor源码)
5.1.1 定义
泛化仿函数:将C++所允许之任何处理请求封装起来以获得具备类型安全性质(typesafe)的高级对象。(定义参考【Andrei 2001】)
5.1.2 泛化目的
泛化仿函数目的是存储“处理请求”,并能过作为参数传递以无限延迟其调用时间。简单归纳为以下三点:
1、可封装任何处理请求,因为它可接受函数指针、成员函数指针、仿函数,甚至其他泛化仿函数,并且参数的个数不确定——连同它们的某些参数或全部参数。(泛化参数类型以及个数不限制性)
2、具备型别安全性(typesafe)。因为它不能允许将错误的参数类型匹配到错误的函数上。(对象封装的好处)
3、是一种带有“value语义”的对象,且允许被任意拷贝而不暴露其内部成员。(对象封装好处,“value语义”是指支持拷贝,赋值,传值操作。)
其实泛化仿函数就是函数指针的一个高阶形态,能够提供operator()且比函数
指针高阶以具备上面提到的三点。
5.1.3 适用场合
当你的程序中的算法或者成员函数拥有相同的名字但是行为不同的时候你就可以使用泛化仿函数,它能将你的行为信息隐藏,这样使用是不就不必知道过多关于某个算法或者成员函数的具体信息(例如参数个数以及类型)。
基本上你能用到仿函数的地方都能用泛化仿函数,泛化仿函数能很大程度上提高你的代码复用性,即便你在改变你算法行为或者参数以后也不需要重新编译调用端程序。
5.1.4 泛化仿函数(functor)结构
只要是支持operator()的所有构件都可以归入泛化仿函数之列。
(后期画图预留位置)
下面给出一个典型的简化了的泛化仿函数结构代码。
class functor
{
public:
void operator = (functor &tmp) ; //提供赋值语义
void operator() () ; //提供operator()操作
functor(functor &tmp); //提供拷贝语义
functor(); //构造函数
~functor(); //析构函数
};
5.2 泛化仿函数的不足
任何泛化作品必将收到效率的质疑,泛化仿函数也一样,虽然我们已经在前面提到的编译期操作提高了一些效率,但是灵活和效率总是天平两端,只有根据你自己的程序实际情况选择不同的解决方法。如果你程序中行为的改变很多,而你也不希望时常因为行为的改变而重新编译调用端程序,那使用泛化仿函数将是你比较好的选择。如果你的程序是实时性比较高或者要求算法效率很高的,那就希望你自己斟酌是否需要使用泛化仿函数了。
5.3 实作泛化仿函数
5.3.1 泛化仿函数template主体
泛化仿函数其实是保存一些行为,且在合适时候合适地点调用行为的算法,从字面上理解,其实也是对行为的一种封装,不过不一样的是我们的仿函数需要有“value语义”,这也就说我们必须将行为保存下来以支持拷贝,赋值等操作。根据“value语义”的泛化仿函数框架应该是这样的。
class Functor
{
public:
void operator = (Functor &tmp) ; //提供赋值语义
void operator() () ; //提供operator()操作
Functor(Functor &tmp); //提供拷贝语义
Functor(); //构造函数
~Functor(); //析构函数
};
这仅仅是一个框架,里面并不包括我们将要用来保存行为的变量,但是一切都会有的。
一个提供operator()的对象或者函数指针是会有返回值的,而且关键的是你并不一定知道将是什么类型的返回值,也许第一次接触泛型的人来说这些考虑似乎比较不能理解,但是在泛化的世界里我们必须保持给予程序最大的可复用性。所以返回值有可能是void或者std::list,这样我们就必须在模板参数中腾出一个位置来给返回值
template<typename ResultType>
class functor;
接下来是operator()的参数,为了能给程序员带来最大的便利,我们必须假设operator()参数个数是无限制且类型也是无限制的,这就遇到麻烦了,类型的无限制可以用模板解决,但是参数个数无限制该怎么办。参照了STL源码以后或许会得到一些灵感,根据参数个数不同我们可以写出若干个operator()的重载函数,当调用时候编译器会根据参数个数不同而自动选择调用的重载函数体。想到这里我们也许会感到原来事情是那么简单,我们可以写出下面的代码:
template<typename ResultType,typename ParmType>
class Functor
{
public:
//...some member function...
ResultType operator() ()
{
// ...do something...
}
ResultType operator() (ParmType p1)
{
// ...do something...
}
ResultType operator() (ParmType p1,ParmType p2)
{
// ...do something...
}
//...and more operator() function...
};
也许你会为了写出上面的代码而欢呼雀跃,毕竟向成功距离不远了,但是当你冷静下来试图自己使用自己的泛化仿函数的时候你会发现其实似乎并不是那么好用,原因出在operator()的参数类型上,你不能指望程序员在使用你的泛化仿函数时候operator()的所有参数都是一个类型。这又是一个大麻烦,模板参数个数不定将怎么实现?你不可能每个operator()定义一个模板,因为C++规定相同函数名的模板参数个数必须是一样的,否则编译器无法通过你的模板参数选择该执行哪一个重载体。
5.3.2 TypeVector带来了曙光
模板参数个数限制了我们前进的脚步,不过还好C++是一门无所不能的语言,它总是能让你灰心后又燃起希望。
我们先前介绍TypeVector的时候就介绍过这个类型容器是为了解决模板参数个数不确定这个问题,TypeVector容许可变大小的存储类型,并且将这些类型以一个容器的形式发给模板参数,这样就可以使得我们的程序并不知道模板参数的个数,因为它只需要接收一个容器。
加入了TypeVector以后我们刚才写的程序就可以变得趋近合理。
template<typename ResultType,typename TVector = TypeVector<> >
class Functor
{
typedef TVector ParmList;
typedef ResultType R;
typedef TypeAt<TypeVector,0> parm0;
typedef TypeAt<TypeVector,1> parm1;
typedef TypeAt<TypeVector,2> parm2;
//...and more typedef...
public:
//...some member function...
ResultType operator() ()
{
// ...do something...
}
ResultType operator() (Parm0 p1)
{
// ...do something...
}
ResultType operator() (Parm0 p1,Parm1 p2)
{
// ...do something...
}
//...and more operator() function...
};
需要注明的是,为了减少文章的篇幅,我假设我们在使用operator()时候至多只有两个参数,azure库中是三个参数,你可以写得更多,但是实际中超过三个参数的函数也是不常用的。
上面的程序使得我们可以写出下面的代码:
Functor<int,TypeVector<int,double> > myFunctor;
TypeVector是很优秀,它把做到了一些似乎不可能的事情:将若干类型存储以后转发。对此我还很记忆犹新,在某聊天群里,一些程序员发出惊讶的语言,以表示不相信C++能办到这个,但是它的确办到了。
5.3.3 储存行为
为了能提供“value语义”,我们必须将行为在Functor对象构造的时候保存起来,但是行为是未知的,所以我们很容易的想到在构造函数上加一个模板参数:
template<typename ResultType,typename TypeVector>
class Functor
{
//... more...
FUN fun;
public:
template<typename FUN>
Functor(FUN &fun_tmp): fun(fun_tmp){};
//... more...
};
老问题解决了,伴随着新问题出现了,在上面代码中由于只有在构造函数调用时候才能确定FUN,所以在之前Functor中的成员变量fun不知道其具体类型,这就导致编译器的报错。想到这,有些人就会马上意识到,如果我们将FUN加入Functor的模板参数中:
template<typename ResultType,typename TypeVector,typename FUN >
class Functor
{
//... more...
FUN fun;
public:
Functor(FUN &fun_tmp): fun(fun_tmp){};
//... more...
};
这是可行的,但不是漂亮的,因为你在构造时候不可能指望你能明确知道FUN是什么样子的,你也许只知道其对象是什么样子的。而且将这个FUN写入类模板参数后就丧失了编译器在构造函数参数中自动推导FUN的能力。这个损失将是致命的,本来想简化程序员的操作,反而加重了程序员的操作,程序员必须写出下面的代码才能通过编译。
struct TestFunctor
{
void operator () (int i , double j ,int k) { std::cout<<"TestFunctor("<<i<<","<<j<<","<<k<<")"<<std::endl; }
};
TestFunctor test_f;
Functor<void,TypeVector<int,double,int>,TestFunctor>(test_f) fun;
如果TestFunctor是一个带模板的类,那你还必须把TestFunctor的模板参数写入Functor的模板参数中,这将变成一个难看的模板嵌套,而且看上去是多余的,因为我们其实已经得到test_f了,并不需要知道TesrFunctor的具体形态。
要改观现状,我们就必须另外定以一个类用来专门保存FUN行为,这样Functor内部就不需要维护FUN行为了。但是具体代码比想象会复杂一点,因为为了让Functor真正不知道FUN,就必须用到多态和偏特化,下面我将慢慢分析我的代码。(注:思路来自Loki和boost::any)
在Functor中我们将维护一个FunMem的指针,这个指针将被初始化执行其继承类FunctorHandler,这样我们将FUN行为传给FunctorHandler,然后用FunMem取用FUN行为,这得利于C++多态(想了解更多关于多态等内存布局建议阅读《深度探索C++对象模型》)。
template<typename ResultType,typename TypeVector>
class Functor
{
//...some typedef...
FunMem<ResultType,TVector> *spFun;
public:
template<typename Fun>
Functor(Fun& fun): spFun ( new FunctorHandler<Fun,Functor>(fun) ) {};
//...some member function...
ResultType operator() ()
{
return (*spFun)();
}
ResultType operator() (Parm0 p1)
{
return (*spFun)(p1);
}
ResultType operator() (Parm0 p1,Parm1 p2)
{
return (*spFun)(p1,p2);
}
//...some member function...
};
类似上面这个技法在boost::any中得到淋漓尽致的发挥,使得boost::any可以接收任意类型数据。
5.3.4 FunMem和FunctorHandler
如上一节所见,将行为FUN保存于FunMem中,然后调用FunMem::operator(),
所以FunMem中必须有一个operator()。
这样我们可以写出下面的代码:
template<typename ResultType , typename TVector>
struct FunMem
{
typedef TypeAt<TVector,0> parm0;
typedef TypeAt<TVector,1> parm1;
virtual ResultType operator () ()=0;
virtual ResultType operator () (parm0)=0;
virtual ResultType operator () (parm0,parm1)=0;
virtual ~FunMem() {};
};
然后FunctorHandler将继承FunMem,并且重写上面的纯虚函数,FunMem中将析构函数定义成虚函数是防止继承类析构时候无法析构虚基类的问题。
FunctorHandler的代码将是下面这样的:
template< typename FUN , typename Functor_ >
class FunctorHandler :public FunMem<typename Functor_::R,typename Functor_::ParmList>
{
FUN _fun;
public:
FunctorHandler(FUN fun):_fun(fun) {};
typename Functor_::R operator() ()
{
return _fun();
}
typename Functor_::R operator() (typename Functor_::Parm1 p1)
{
return _fun(p1);
}
//再次提醒,参数假设最多只有2个
typename Functor_::R operator() (typename Functor_::Parm1 p1,typename Functor_::Parm2 p2)
{
return _fun(p1,p2);
}
};
采用这样的代码结构的好处是,FunMem的确不知道FUN,但是FunMem的指针却可以指向FunctorHandler后调用其operator(),在Functor中维护一个FunMem指针就达到维护FUN行为的目的了。
我总是希望更多C++初级读者能融入我的思路,所以会显得我的程序总是写出来以后再推到,例如上面的FunMem其实在编译期会报错,报错提示是:“类不会将“operator()”或用户定义的转换运算符定义到指向函数的指针或指向函数的引用(它们接受适当数量的参数)”。至于为什么会这样报错就连【Andrei 2001】中也没有提到,【Andrei 2001】只是给出了正确的解决方法,为了了解为什么会报错,我查阅了很多关于泛型和Template的书籍,也在网上询问了很多高手,但是都没有得到解决,不过皇天不负有心人,在无意中翻阅【Lippman 96】时候发现template在特化虚函数时候和特化普通函数不一样,当普通函数实际当中没有被使用到的时候编译器就不会特化出其代码,但是虚函数无论是否被使用都必须特化出其代码,所以这里我们FunMem中的operator()几个重载体都被特化出来了,而当FunctorHandler调用operator()时候会发现FUN也许需要传入2个参数,而FunMem却定义了传入0个参数的操作,所以编译器就会报出使用错误,虽然这个错误看起来有点无厘头,但是这就是模板报错特点,总是偏离真正错误位置。
为了解决这个情况,我们必须保证FunMem中只有一个operator()虚函数,且这个函数肯定被使用,是否听起来很不可思议,但是的确能办到,如果我们先采用模板偏特化筛选FunMem就可以,下面给出实际代码:
template<typename ResultType , typename TVector>
class FunMem
{
};
template<typename ResultType>
class FunMem<ResultType,TypeVector<> >
{
public:
virtual ResultType operator () ()=0;
virtual ~FunMem(){};
};
template<typename ResultType ,typename p1>
class FunMem<ResultType,TypeVector<p1> >
{
public:
virtual ResultType operator () (p1)=0;
virtual ~FunMem() {};
};
template<typename ResultType, typename p1, typename p2>
class FunMem<ResultType,TypeVector<p1,p2> >
{
public:
virtual ResultType operator () (p1,p2)=0;
virtual ~FunMem() {};
};
当给定参数个数时候FunMem模板偏特化就会确定出是哪一个类,并且其余类都不会真正产生代码,每个FunMem中只有一个虚operator(),并且肯定是参数个数正确的那一个operator(),这正好满足我们前面提到的要求。
5.3.5 完整的Functor
现在有了FunMem和FunctorHandler,我们就可以完整实作Functor了,Functor加入对赋值拷贝操作支持后的完整代码将是下面这样的:
template <typename ResultType, typename TVector = TypeVector<> >
class Functor
{
//当我们接收一个TList以后,我们其实很希望从里面提取出各个型别,然后作为参数用,这就可以用到先前我们定义的那一系列编译期的类操作了
public:
typedef TVector ParmList;
typedef ResultType R;
typedef typename TypeAt<TVector,0>::Result Parm1; //美妙吧,一切操作都是编译期执行完,我们并没有产生一个类
typedef typename TypeAt<TVector,1>::Result Parm2;
typedef typename TypeAt<TVector,2>::Result Parm3;
//类似这样的可以无限添加
//进行到这里,我们理理思绪,如果我们泛化了这个仿函数,那以后我们希望看到这样的形式
// struct TestFunctor
// {
// void operator ()(int i,double j) { ... };
// };
// int main()
// {
// TestFunctor f;
// Functor<void,TYPELIST_2(int,double)> cmd(f);
// cmd(3,1.8);
// }
//这的确是一个很漂亮的程序,我们将TestFunctor所要做的操作保存,并且无限推后,可是如果要达到这种形式,我似乎要写一个
//能够接收TestFunctor的构造函数,在泛化世界里,我们应该写一个可以接收任何类的构造函数。
//在Andrei.2000中为了解决这个问题做了一个很好的FunctorImpl类,这个类定义了一系列的纯虚函数,所以有很强的复用价值,但是作为研究,我为了
//不让大家越绕越晕,所以想到用了比较简单的方法,就像下面这个。
/*
template <typename Fun>
Functor(const Fun& fun)
{
//我们需要一个容器能够保存传入的类,以提供以后使用
Fun *TempFun = &fun;
}
*/
//噢,NO,这里突然发现一个问题就是似乎无法将fun保存下来,因为Functor类似乎并不知道Fun这个类,所以该怎么办?
//知道很多人会想到,那吧Fun加入最开始的那个模板参数中,这样就可以定义一个私有成员变量以保存了,但是大家会发现这样还是
//会失败,因为这个类并没用到这么多模板参数,那个参数只是构造函数用到,这样就又犯了一个错误就是函数不支持模板偏特化
//面包会有的,牛奶会有的。只要我们不断的发挥想象,如果这个东西不能在Functor中保存,那我们可以在之外保存啊,对了,我们可以把
//他先传到外面,然后再回来拿来用,虽然过程有点曲折,但是暂时似乎我还没想到更好的办法,也许等到若干年后我又会说:“当时我怎么
//这么愚蠢,用了这么愚蠢的方法。”。不过这就是C++的魅力,它迫使你不断的动脑以想更简单更有效率的方法以替代过去的
//那我们开始我们的奇幻旅程吧
//我打算在Functor外部定义一个叫做FunMem的类,其义为Fun中的成员.
//那构造函数看起来似乎就是这样
FunMem<ResultType,TVector> *spFun;
public:
template<typename FUN>
Functor(FUN& fun): spFun ( new FunctorHandler<FUN,Functor>(fun) ) {};
//下面就是我们类里面比较重要的部分,我们将重载()运算符,已达到仿函数目的
ResultType operator () ()
{
return (*spFun)();
}
ResultType operator () (Parm1 p1)
{
return (*spFun)(p1);
}
ResultType operator () (Parm1 p1 , Parm2 p2)
{
return (*spFun)(p1,p2);
}
//为了简化代码量,我假设至多只有2个参数,当然你可以定义很多很多而只是复制粘贴代码而已
void operator= (Functor Fctor)
{
spFun = Fctor.spFun;
}
};
使用上面的程序在我们自己的代码将会看到是这样的:
struct TestFunctor
{
void operator () (int i , double j ,int k) { std::cout<<"TestFunctor("<<i<<","<<j<<","<<k<<")"<<std::endl; }
};
int main()
{
TestFunctor f;
Functor<void,TypeVector<int,double,int> > cmd(f);
cmd(1,1.8,5);
getchar();
return 0;
}
当我们使用cmd的时候不需要其中具体的行为是什么,而只需要传入参数,我们改变cmd中的行为,也只需要重新定义一个Functor对象且重新传入行为,而调用cmd端的代码是不用改变的,这就达到了提高代码的复用性。
5.4 一切都还没结束
不断最求简便和通用是泛型程序的一贯目标,在上面的代码中似乎我看到了一些还可以简化的东西,既然不需要改变cmd调用端的程序,那我们为什么不让cmd的operator()参数个数总是为0,因为不需要传入参数是最简便和最通用的调用方法。
如果我们办到像前面提到的一样将变得更灵活,我不但可以控制一个仿函数的运行时间,还能控制其运行参数。刚开始想这个的时候让我想到了采用参数默认值似乎能达到同样的目的,但是其实采用绑定参数方法会使程序较之灵活得多
,当你采用默认参数时候,如果你想修改这个默认参数就必须修改以前的函数库或者程序,也就必须重新编译一次程序,而采用绑定就可以使这一切推迟到终端上,这就使修改很方便,你可以随时选择重新绑定参数(关于绑定的更多信息可以参阅STL源码)
为了写出类型上面Bind这样的功能,我们想一下,首先肯定要在Functor里面重载一个"="运算符,其次我们就要做Bind类,Bind的返回值肯定是一个Functor class,而第一个参数是Functor class,第二个参数是需要绑定的数据类型。在理论上泛型是不限定参数个数的,也就是这里我们最希望能灵活的选择绑定参数的个数。不过还是让我们从基础做起吧,先对一个参数绑定,个数无限也就能办到了。Bind这个名字其实很有歧义,在STL中叫做BindFirst代表绑定第一个参数的意思,我将沿用这个名字。
简单的看,BindFirst的作用应该是保持需要绑定的第一个参数,以及将需要保存的Functor对象里的ParmList的第一个类型删掉,最后再返回这个Functor。由于在先前的代码中已经提供了怎么保持FUN的方法,所现在写这个程序思路会比较清晰,下面我们实作一个BindFirstFunctorHandler类,这个类的作用是保存第一个参数以及在每次调用operator()的时候把保存的参数放进去调用。因为BindFirstFunctorHandle同样是对若干operator()的重载,且需要调用FUN行为,所以BindFirstFunctorHandle中必须包含一个FunMem成员以用于调用FUN行为。
由于整体比较简单,所以我就不单步给出代码,而直接给出BindFirstFunctorHandler的完整代码。
template< typename Functor_ ,typename FirstType,typename ParmList>
class BindFirstFunctorHandler
{
FirstType FirstValue;
FunMem<typename Functor_::R, typename Functor_::ParmList> *spFun;
public:
//构造函数,第一个参数是需要绑定的FUN行为,由于无法直接保存FUN行为,所以就保存FunMem指针,//第二个参数是需要绑定的参数值。
BindFirstFunctorHandler(FunMem<typename Functor_::R, typename Functor_::ParmList> *_spFun,FirstType FValue):spFun(_spFun),FirstValue(FValue) {};
//这是绑定后没有参数个数的()重载,由于绑定了第一个参数,所以只需要把第一个参数传给spFun
//就行了
typename Functor_::R operator() ()
{
return (*spFun)(FirstValue);
}
typename Functor_::R operator() (typename Functor_::Parm2 p2)
{
return (*spFun)(FirstValue,p2);
}
};
在BindFirstFunctorHandler中的operator()只重载了两个版本,是因为绑定以后参数会减少一个,所以原先三个重载版本绑定后就只有两个。其实就是保存一个参数等到调用时候直接传入。
只是BindFirstFunctorHandler还是不够的,BindFirstFunctorHandler只是为我们提供了绑定FUN参数的操作,但是我们还需要返回一个Functor对象,这个对象和绑定前的对象的差异就需要对绑定前的Functor中的ParmList删除第一个类型。所以BindFirst的返回值是这个样子的:
Functor<typename Fctor::R,typename ErasePose<typename Fctor::ParmList,0>::Result >
其中Fctor::R是返回值,ErasePose<typename Fctor::ParmList,0>::Result是删除了原本的ParmList中第一个元素后的ParmList。下面就看看BindFirst函数的实作。
template <typename Fctor,typename FirstType>
Functor<typename Fctor::R,typename ErasePose<typename Fctor::ParmList,0>::Result > BindFirst(Fctor _Functor,FirstType FirstValue)
{
typedef typename Fctor::R Result;
typedef typename Erase<typename Fctor::ParmList,Parm>::Result TVector;
Functor<Result,TVector> _RFunctor(*(new BindFirstFunctorHandler<Fctor,FirstType,TVector>(_Functor.spFun,FirstValue) ) );
return _RFunctor;
}
BindFirst做完了,下一节我将演示怎么使用我们的作品,让你看到泛化的魅力。
5.5 泛化仿函数的使用
当我完成Functor的时候我是异常的兴奋,因为这是我一个很好的开始,也是一个比较难的开始,TypeVector也只是我最后才创作出来的,开始一直用的TypeList,所以Functor是我真正意义上的第一个毕业设计程序,下面我们看看有了它将会带来多大的便利。
struct TestFunctor
{
void operator () (int i , double j ,int k) { std::cout<<"TestFunctor("<<i<<","<<j<<","<<k<<")"<<std::endl; }
};
假设有上面一个行为,输出传入的参数。那我们的主函数可以变成这个样子的。
int main()
{
//定义行为f
TestFunctor f;
//构造Functor对象cmd并初始化为行为f
Functor<void,TypeVector<int,double,int> > cmd(f);
//将第一个参数值为1绑定在cmd上并初始化到cmdBind_1上
Functor<void,TypeVector<double,int> > cmdBind_1 = BindFirst(cmd,1);
//继续绑定
Functor<void,TypeVector<int> > cmdBind_2 = BindFirst(cmdBind_1,1.8);
//这时候所有参数都绑定完
Functor<void> cmdBind_3 = BindFirst(cmdBind_2,5); //第二个模板参数默认值是TypeVector<>,代表没参数
cmd(1,1.8,5);
cmdBind_1(1.8,5);
cmdBind_2(5);
cmdBind_3(); //这就是完全绑定以后的样子,看似没有参数,但是其实只是所有参数都被绑定了
getchar();
return 0;
}
在主函数的中可以看到我们用TestFunctor f;创建出一个行为TestFunctor的对象f,让后用我们的Functor封装f得到cmd。而在后面我们只要调用cmd(1,1.8,5);就能得到等同于f(1,1.8,5);的效果,而最主要的是如果你需要改变行为,只需改变最开始的Functor初始化那,而不用改变后面的cmd使用,这使得无论在维护还是修改都方便很多,毕竟如果cmd是分独立到运行端的就不用再重新编译运行端代码了。
而在后面用到 BindFirst函数将第一个参数绑定以后可以通过调用cmdBind_1(1.8,5);得到和cmd(1,1.8,5)一样的结果,直至所有参数完全绑定完得到cmdBind_3()。
5.5.1 完全绑定的好处
完全绑定的用途1: 有时候我们需要记录一些已经处理过的动作,然后当用户需要的时候来个ReDo,那么我们只需要执行一次,就可以把所有记录的操作完成,这就像我们把用户的操作记录压栈(其实就是逐个绑定),然后只需要执行一个cmd,因为运行环境我们已经保存了。在很多比较智能的文档编辑器中,就提供了Redo和Undo的操作,只要有了绑定记录用户的一系列操作,这就解脱程序员很多工作。
完全绑定的用途2: 假设我们有一个很智能化的家,家中有自动洗衣机,只需要你一个命令就运行,家中有电饭煲,也只需你一个命令。
洗衣服过程应该是(1)放入洗衣粉(2)启动洗衣(3)脱水。
煮饭过程应该是 (1)淘米 (2)启动煮饭(3)焖饭。
但是如果采用绑定,我们分别把洗衣机的过程逐个绑定进入洗衣机类,并且用Functor推迟到cmd命令,那家中的电脑只要接到cmd命令就可以开始洗衣服。当你想煮饭的时候你依然把煮饭过程逐个绑定进煮饭类,然后用functor推迟到cmd命令,那家中电脑只要接到cmd命令就会开始煮饭,关键的地方是,家中电脑并不知道cmd是什么样子的,也就是并不知道是将要煮饭还是洗衣服,这一切都是有用户来发送,家中电脑只需要执行cmd就可以完成用户想要的工作。
如再灵活一点,有一天下雨,你把衣服淋湿了,而你不想洗衣服,因为是羊毛衫,而只想脱水,那这个时候你就可以只需要绑定一个脱水命令,然后传给cmd给家中电脑执行。也许有一天你想蒸馒头,所以不需要淘米和焖饭,所以你只需要绑定一个煮饭命令,然后传给cmd命令给家中电脑执行。也就是说家中的电脑不需要知道具体要干什么,他只需要接收cmd命令执行,什么事都是用户自由绑定。
多么美好的世界啊~~~~~我希望有一个这样的家。我老婆(其实我现在单身)就不会向我抱怨洗菜煮饭洗衣服的痛苦了,她只需要操控遥控板。
5.6 摘要
l 仿函数是一种封装了某种支持operator()调用的行为,并且可以动态的支持参数个数以及类型的不定。参数的类型是用TypeVector封装(TypeVector细节见地4章)。
l Functor相比函数指针式具备安全型别的。
l Functor支持value语义,但是Functor不支持扩展,无多态。所有扩展以及多态都是在FunMem中。
l Functor支持参数的绑定,绑定函数为BindFirst,你也可以参照BindFirst实作出Bind2nd等,这里我只是想阐述一种思想,并不是真正提供一个库函数。
l Functor的使用需要精打细算,因为每次Functor的调用至少需要一次虚表的查询操作,请在你觉得性价比合适的时候使用。