STL中伪函数
伪函数或者函数对象只是翻译的问题,英文全部都是functor,还有些读物管这个叫函数符。functor作为C++ STL六大基本组件之一被广泛使用(其他五个分别是容器、演算法、迭代器、适配器、分配器),那么,functor的意义在哪里?或者说它有什么作用?
这项技术从概念上不难理解,伪函数也好,函数对象也好,都说明了它不是真正的函数,而是一种类似函数作用的东西,在STL里,我们经常需要对容器内元素进行批量操作,手写循环确实是个方法,但是,代码冗余,而且效率普遍偏低,这就需要一个批量操作方法,能够对容器内的元素进行批量操作,那么,怎么操作,操作什么,这就需要用户去定义了,你需要告诉程序,你想要什么样的操作,一般人对于这种问题的第一反应就是函数指针,确实,通过传递一个函数指针可以让程序执行特定的操作,这也确实是一个方法,但是按CPP Primer Plus上的原文来说,它有一个很大的弊端,那就是函数指针是必须指定参数以及返回类型的,而STL属于泛型编程,加入用函数指针来操作的话,是违反泛型思想的本质的,因此,这就需要一个能取代函数指针的东西了,而这个东西就是所谓的函数对象。
函数对象是一种介于对象和函数之间的一个东西,调用方式和函数一样,但是调用的却是对象里的operator ()操作符,这可以给程序带来三大好处。第一,面相对象里的模板机制对functor是完全适用的,因此用函数对象可以和STL完全兼容,换句话说,使用functor和泛型的思想相符。第二,函数对象本质上是对象,因此,一个函数对象可以使用类中所封装的所有数据,而对于函数指针来说,只能使用全局数据,所以,使用functor可以达到数据封装的目的。第三,functor往往是轻量级代码,因此,可以完美内联化,这对于程序效率的提升是非常有利的。基于以上三点,就不难理解为什么functor是STL六大组件之一。函数对象可以带来这么多好处,那么在STL编程中当然是要尽量多用了,那么现在问题来了,假如类在设计的时候没考虑到和STL的适配怎么办?这就需要用到STL functor适配器了,常见的适配器有三个:ptr_fun、mem_fun和mem_fun_ref。这三种适配器分别针对不同的对象,ptr_fun可以把一个全局函数转化成一个functor,后两者则分别针对面相对象中的函数和函数指针。
首先看第一个函数适配器,也就是ptr_fun,这个适配器可以把一个全局函数转化成一个functor,根据sgi上的解释,ptr_fun实际上是两个函数,当传递的函数具有一个参数的时候,ptr_fun会接受一个函数指针,然后返回一个unary_function类型的函数指针,而传递的函数具有两个参数的时候,它则会返回一个binary_function类型的函数指针。其实,ptr_fun并不神秘,同STL种大多数适配器一样,它只不过是让一些函数实现所需要的typedef生效而已,而其他两个适配器也是具有同样的作用。
另外两个适配器都是针对类中的函数,假如容器中所放的是实体,那么转换函数符的适配器应该使用mem_fun_ref,假如容器中所存放的是指针,那么函数符适配器应该使用mem_fun,这是二者唯一的区别。不论是mem_fun_ref还是mem_fun,都接受一个函数指针,然后返回一个函数符指针,由于global function本身就是一个函数指针,因此不需要做取址操作,但是对于对象来说,则必须加“&”操作符,也就是说,在mem_fun_ref内部,必须在函数名前面加上&,这样才可以达到传递指针的目的。
好了,有了以上理论知识,就可以探讨一下函数对象的用法了,接下来我会以STL中使用最频繁的for_each做例子,来说明函数对象的原理和作用。
先查一下for_each的用法,在www.cplusplus.com上查到了比较详细的解释,原文如下:
Apply function to range
Applies function f to each of the elements in the range [first,last).
The behavior of this template function is equivalent to:
template<class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f) { for ( ; first!=last; ++first ) f(*first); return f; }
Parameters
- first, last
- Input iterators to the initial and final positions in a sequence. The range used is [first,last), which contains all the elements between first and last, including the element pointed by first but not the element pointed by last.
- f
- Unary function taking an element in the range as argument. This can either be a pointer to a function or an object whose class overloads operator().
Its return value, if any, is ignored.
Return value
The same as f.
从上面这段文字首先可以看出,for_each是个区间操作函数,输入的前两个参数是起始迭代器和终点迭代器,而第三个参数则是一个function类型的参数,从下面的解释不难看出,这个参数既可以是函数,又可以是函数对象(也就是重载了()的类),也就是说,虽然函数对象在STL中很多地方已经取代了函数指针,但并不是函数指针就被完全抛弃了,因此,for_each所操作的对象有两种:函数对象和全局函数指针,以此类推,STL中所有其他range function也都具有相似的特征呢?很有可能,而事实上,几乎所有区间操作函数确实也全都是这样的。
好,有了以上的解释,我们就可以试试for_each的用法了。先从最简单的情况开始,试一试一元全局函数+函数指针的情况,看看以下代码片段。
- #include <iostream>
- #include <vector>
- #include <iostream>
- #include <algorithm>
- using namespace std;
- void printTest(const int &data)
- {
- cout << data << endl;
- }
- int main()
- {
- int test[] = {1, 2 , 3 , 4 , 5};
- vector<int> v(test, test + sizeof(test) / sizeof(int));
- for_each(v.begin(), v.end(), printTest);
- return 0;
这个代码片段中,给for_each传的参数是全局函数指针,可以通过编译,这也说明了STL可以接受正确的函数指针作为参数,那么,可不可以把这个函数指针转化成functor?当然可以,而使用的方法正是之前提到的,用ptr_fun来转,也就是这样:
- for_each(v.begin(), v.end(), ptr_fun(printTest));
仍然可以通过编译,也就是说,正确的全局函数指针转化成函数符是没有问题的。OK,下面试一下binary glocal function的情况,把原代码做一下微调,变成下面的样子:
- #include <iostream>
- #include <vector>
- #include <iostream>
- #include <algorithm>
- using namespace std;
- void printTest(const int &data, const int &num)
- {
- int result = data+num;
- cout << "data:"<<data<<"num:"<<num<<",result:"<<result << endl;
- }
- int main()
- {
- int test[] = {1, 2 , 3 , 4 , 5};
- vector<int> v(test, test + sizeof(test) / sizeof(int));
- for_each(v.begin(), v.end(), printTest);
- return 0;
- }
w 变化的部分是printTest函数,由unary function变成了binary function,主函数不变,编译出错,出错的原因很简单,对于带两个形参的全局函数,如果直接把函数指针传给for_each,程序不会知道哪个形参应该接收变量,因此会出错,正确的做法应该是先绑定参数,然后再传给for_each即可,这里,就用到了STL里的参数绑定器,也就是bind1st和bind2nd,拿bind1st做例子,这个函数的声明是这样的:
template <class Operation, class T> binder1st<Operation> bind1st (const Operation& op, const T& x);
再看一下对于参数的描述:
op
Binary function object derived from binary_function.
x
Fixed value for the first parameter of op.
第一个参数op是我们需要的operation,而第二个参数则是需要绑定的参数的值,也就是说,按照这种方式,如果我想在上面的代码中调用bind1st应该是类似这种形式:bind1st(printTest , 3),但是很明显,直接这么用肯定是错的,因为printTest并不是一个继承自binary_function的函数,这里就用到了刚才所用到的ptr_fun了,这个函数可以把global function转化成一个函数对象,而对于有两个参数的函数对象来说,则会返回一个继承自binary_function的函数对象,因此,对于这个程序,正确的绑定应该是这样:bind1st(ptr_fun(printTest) , 3),在这个表达式里,printTest的第一个参数被绑定成了3,好,再次编译,因该没问题了吧?错了,仍然有问题,问题如下:
error C2535: 'void std::binder1st<_Fn2>::operator ()(const int &) const' : member function already defined or declared.
函数存在重复定义,而重复部分函数在STL中的xFunctional头文件中,重复的函数为:
std::pointer_to_binary_function<const int &,const int &,void,void (__cdecl *)(const int &,const int &)>
这里我们知道,ptr_fun可以把全局函数转换成一个pointer_to_binary_function,但是该函数的默认参数正式和printTest具有一样形式的函数,也就是说,两个函数的指针形式是一样的,因此定义重复,我们把原代码再次改一下,把printTest的声明改成这种形式:void printTest(const int &data, int num),再次编译、运行,产生的结果如下:
从截图可以清晰的看到,参数3被绑定到了printTest的第一个参数上,而第二个参数则会接收容器中的变量,该段代码产生的效果和手写循环一样。bind2nd的用法和bind1st一样,只不过它是把变量绑定到函数第二个参数上,假如用bind2nd(ptr_fun(printTest) , 3)代替上面的代码的话,会输出这样的结果:
从截图上可以看到,3被绑定到了第二个参数上。
讨论完了全局函数的情况,该讨论一下类中成员函数的情况了,其实成员函数和全局函数在转化成functor的原理上都是一样的,都是需要借用一个适配器,这个适配器可以让functor所需要的typrdef生效,而成员函数所需要的适配器则是mem_fun和mem_fun_ref,关于这两个函数的区别在上面已经提到,不做赘述,仅举一例来说明这个函数的用法。
在上个程序中,printTest是一个全局函数,可以被直接调用,现在,我们把它封装到一个类中,经过改造后的代码如下:
- #include <iostream>
- #include <vector>
- #include <iostream>
- #include <algorithm>
- using namespace std;
- class print
- {
- private:
- int data;
- public:
- print(){}
- print(int _data)
- {
- data = _data;
- }
- void printTest(int num)
- {
- int result = data+num;
- cout <<"data: "<<data<<" num: "<<num<<" result: "<<result << endl;
- }
- };
- int main()
- {
- print p1(1);
- print p2(2);
- print p3(3);
- vector<print> v;
- v.reserve(3);
- v.push_back(p1);
- v.push_back(p2);
- v.push_back(p3);
- for_each(v.begin(),v.end(),bind2nd(mem_fun_ref<void,print,int>(&print::printTest),3));
- return 0;
- }
那个for_each语句中,mem_fun_ref后面一连串的模板参数让人看起来有点晕,但是一点都不复杂,第一个参数是函数对象的返回类型,在这个程序里,printTest返回的是个void,第二个是调用函数的类,即是print类,第三个是绑定的参数类型,在printTest中就是int。之所以显式声明出模板参数,是为了演示需要,如果你愿意,你完全可以去掉哪些参数,而直接使用mem_fun_ref(&print::printTest),这是完全没有问题的,模板函数不必一直都显式指定参数类型。还有一点需要注意的就是,对于成员函数的变量绑定,肯定是用bind2nd绑定器的,因为默认状态下,第一个变量已经绑定给了调用函数的对象上,因此只有一个变量可以绑定,而mem_fun_ref也明确规定了转化的目标函数只能无参数或者具有一个参数。
函数对象和绑定器是密切相关的,本文对绑定器原理方面并没有做过多的叙述,如果各位有兴趣,可以自行搜索一下binder,以加深对函数对象的理解。