[CPP - STL] functor刨根问底儿
作为STL六大组件之一,在STL源代码及其应用中,很多地方使用了仿函数(functor),尤其在关联型容器(如set、map)以及algorithm(如find_if、count_if等)中。虽然已经接触functor很长时间了,但一直只是编写一些简单的functor,至于为什么要使用functor?functor在STL源代码如何定义?以及如何有效地使用functor呢?源码之前,了无秘密。终于狠下心来,研究了几天STL中的functor,其实functor是STL最简单的一个模块,也可以从此敲打泛型编程的大门。
那么,让我们结合SGI STL源码来一窥究竟吧。
1. functor含义及其优点
functor是仿函数早期的命名,C++标准规格定案后所采用的新名称是函数对象(function object),即一种具有函数特质的对象,好像函数一样可以被调用,其实是利用某些类对象支持operator()的特性,来达到模拟函数调用效果的技术。
考虑到中文用词的清晰漂亮和独特性,“仿函数”更加贴切,所以本文用仿函数或functor代替function object。
先来看一个仿函数与函数调用的实例,
1 /* 函数定义 */ 2 inline bool comparer1(const int& x1, const int& x2) 3 { 4 return a > b; 5 } 6 7 /* 仿函数定义 */ 8 struct comparer2 9 { 10 bool operator()(const int& x1, const int& x2) 11 { 12 return x1 > x2; 13 } 14 }; // 仿函数一般使用struct
用for_each函数说明下,仿函数与函数调用的使用方法,
for_each(v.begin, v.end(), comparer1); // 函数调用(回调函数)
for_each(v.begin, v.end(), comparer2()); // 仿函数
那么,仿函数类似于函数调用,为什么要使用functor呢?
在很多书籍中都会提到,相比于函数调用,functor技术可以获得更好的性能,因为函数始终需要被调用,而functor在编译时会被inline展开(取决于编译器,如果functor中有循环、递归等代码时,functor未必会被展开)。而像函数comparer1即使被定义为inline类型,大部分编译器肯定不会试图去内联展开通过函数指针调用的函数。另外,functor配合STL的适配器(adaptor)组件,可以完美的使用STL。
《Effective STL》中,【条款46:考虑使用函数对象代替函数作算法的参数】中给出了用普通函数和仿函数的性能比较:在不同的STL平台上测量一百万个double的vector的两个sort调用,最差情况下,仿函数快50%,最好的快了160%。
2. STL中的functor
基本的functor有三种:Generator、Unary Function、Binary Function,对应的调用即为无参函数f()、一元函数f(x)、二元函数f(x, y)。
另外,返回类型为bool的Unary Function称为Unary Predicate,返回类型为bool的Binary Function称为Binary Predicate,这可以帮助我们看STL的帮助手册。
STL不支持三元仿函数,所以STL算法的functor参数不会超过两个。当然,也可以根据仿函数适配器(functor adaptor后面会详解)扩展出Ternary Function(三元函数f(x, y, z))及更多参数类型的functor。
在STL中,与functor组件相关的两个部分:内建functor和functor adaptor。其实,functor adaptor属于STL中的适配器组件。
STL中的functor组件无非是提供给其它组件使用,那么要融入整个STL大家庭,所有的functor必须定义自己的型别(associative types)。所有函数或者functor可适配(adaptable)的基础,即两个只包含typedef的空类型的unary_function和binary_function。
unary_function和binary_function在SGI STL中的定义为,
1 template<typename _Arg, typename _Result> 2 struct unary_function 3 { 4 typedef _Arg argument_type; 5 typedef _Result result_type; 6 }; 7 8 template<typename _Arg1, typename _Arg2, typename _Result> 9 struct binary_function 10 { 11 typedef _Arg1 first_argument_type; 12 typedef _Arg2 second_argument_type; 13 typedef _Result result_type; 14 };
程序员想要定义自己的functor,只有派生于这两个类型,向其它STL组件提供型别。
另外,STL也提供了functor adaptor可以将普通函数、类成员函数转换为可适配的函数(adaptable function),并且可以适配、适配、再适配,搭配STL算法完美演出。
一张图示可以简单列举传递给STL使用的所有函数或functor的情况,
图中,未经过适配(橙色箭头)的函数,传递给STL时,没有typedef定义的必要型别,可能会出现编译错误。
当然这里没有画出STL内建的可适配functor,我们就来看看内建的functor吧。
3. STL内建的functor
内建的functor即STL functor组件中预先定义的functor,包括算术运算(plus, minus, multiplies, divides, modulus, negate)、关系运算(equal_to, not_equal_to, greater, less, greater_equal, less_equal)和逻辑运算(logical_and, logical_or, logical_not)。
欲使用内建的functor,必须包含<functional>头文件,SGI则将它们实际定义于<stl_function.h>文件中。
以plus为例,其源代码为:
1 template<typename _Tp> 2 struct plus : public binary_function<_Tp, _Tp, _Tp> 3 { 4 _Tp operator()(const _Tp& __x, const _Tp& __y) const 5 { 6 return __x + __y; 7 } 8 };
在使用STL的时候,可以直接使用内建的functor,如,
accumulate(v.begin(), v.end(), 0, plus<int>()); // 容器v的所有元素的和
与普通的函数模板推演方法一样,首先plus<int>()构造一个对象,传递给算法accumulate,accumulate根据plus对象推演出类类型,然后利用此类类型实例调用运算符()。
4. functor adaptor
adaptable functor即包含派生自unary_function或binary_function的型别,那么本节就来说下如何使函数通过adaptor变为adaptable functor,进而适配、再适配,组合成更加多的表达式。
首先,有必要了解下adaptor的基础知识。
adaptor在设计模式上的定义:将一个class的接口转换为另一个class的接口,使得原本因为接口不兼容而不能合作的classes可以一起工作。
functor adaptor属于适配器的一种,顺便提下,STL提供多种adaptor,其中,
改变迭代器(iterator)接口的称作iterator adaptor(insert iterators、reverse iterators等); |
改变容器(container)接口的称作container adapter(queue和stack); |
改变仿函数(functor)接口的称作functor adapter; |
functor adaptor是所有适配器中数量最庞大的一个族群,这些配接操作包括bind、negate、compose以及对一般函数或成员函数的修饰。其价值体现在,通过它们之间的绑定、组合和修饰能力,几乎可以无限制地创造出各种可能的表达式,搭配STL算法完美演出。如:
not1(bind2nd(greator<int>(), 10)); // 将greator<int>的第二个参数绑定为10,再加上否定操作,形成“不大于10”的functor
那么,所有期望获得适配能力的组件,本身都必须是可适配的,即一元仿函数必须继承自unary_function、二元仿函数必须继承自binary_function。
所以,可以将STL提供的adaptor分为两类:一类是将现有的普通函数和类成员函数转变为adaptable functor;另一类将已经具备适配能力的functor适配、再适配,利用现有的functor组合出无与伦比的函数。
4.1 adaptable functor
先给出如何利用C函数和C++类成员函数输出容器所有元素的方法,
1 #include <algorithm> 2 #include <functional> 3 #include <iostream> 4 #include <vector> 5 6 using namespace std; // 此处冒犯了coding style 7 8 void cPrint(int element) 9 { 10 cout << element << " "; 11 } 12 13 class A 14 { 15 public: 16 A(int element) : m_element(element) 17 { 18 } 19 20 void cppPrint() const 21 { 22 cout << m_element << ","; 23 } 24 25 private: 26 int m_element; 27 }; 28 29 int main() 30 { 31 vector<int> v(5, 2); 32 33 // 直接利用C函数 34 for_each(v.begin(), v.end(), cPrint); 35 cout << endl; 36 37 // 利用处理后的C函数 38 for_each(v.begin(), v.end(), ptr_fun(&cPrint)); 39 cout << endl; 40 41 // 利用处理后的C++函数 42 for_each(v.begin(), v.end(), mem_fun_ref(&A::cppPrint)); 43 cout << endl; 44 45 return 0; 46 }
这段代码中,将会有两个疑问!
1)类比普通函数cPrint,为什么不能直接使用类成员函数cppPrint呢? for_each是这样定义的, 1 template <typename _InputIter, typename _Function> 2 _Function for_each(_InputIter __first, _InputIter __last, _Function __f) 3 { 4 for (; __first != __last; ++_first) 5 __f(*__first); 6 7 return __f; 8 } for_each函数将会直接使用推演出的函数指针,如果是类成员函数(A::cppPrint),将会出现编译错误,不仅for_each,STL的一个普遍习惯是函数和函数对象总使用用于非成员函数的语法形式调用。 |
2)ptr_fun和mem_fun_ref是何物? STL functor组件提供了六个这样的函数,分别是ptr_fun、mem_fun、mem_fun_ref,每一个函数都有一个重载实现。其中ptr_fun的两个实现可以转换一元和二元的普通函数,而mem_fun和mem_fun_ref的重载实现又称为mem_fun1与mem_fun1_ref。 ptr_fun可以将普通函数转换为函数对象pointer_to_unary_function或pointer_to_binary_function,这些函数对象的operator()接受参数和原函数相同。 mem_fun可以将类成员转换为函数对象mem_fun_t,这些函数对象的operator()接受类对象的指针。 mem_fun_ref可以将C++类成员函数转换为函数对象mem_fun_ref_t,与mem_fun_t类似,只是它们的operator()接受类对象的引用。 好吧,还是让源代码来说话吧。 ptr_fun的一元函数转换实现为, 1 template<typename _Arg, typename _Result> 2 inline pointer_to_unary_function<_Arg, _Result> ptr_fun(_Result (*__x)(_Arg)) 3 { 4 return pointer_to_unary_function<_Arg, _Result>(__x); 5 } 6 7 template<typename _Arg, typename _Result> 8 class pointer_to_unary_function : public unary_function<_Arg, _Result> 9 { 10 protected: 11 _Result (*_M_ptr)(_Arg); 12 13 public: 14 pointer_to_unary_function() 15 {} 16 17 explicit pointer_to_unary_function(_Result (*__x)(_Arg)) : _M_ptr(__x) 18 {} 19 20 _Result operator()(_Arg __x) const 21 { 22 return _M_ptr(__x); 23 } 24 }; 根据源代码分析下ptr_fun(&cPrint)的调用过程,很明显,ptr_fun可以返回一个STL需要的仿函数pointer_to_unary_function,然后for_each可以根据此仿函数进行推演,并利用推演出的类类型的实例调用运算符()。
mem_fun_ref的无参类成员函数转换的实现为, 1 template<typename _Ret, typename _Tp> 2 inline mem_fun1_ref_t<_Ret, _Tp> mem_fun_ref(_Ret (_Tp::*__f)()) 3 { 4 return mem_fun_ref_t<_Ret, _Tp>(__f); 5 } 6 7 template<typename _Ret, typename _Tp> 8 class mem_fun_ref_t : public unary_function<_Tp, _Arg> 9 { 10 public: 11 explicit mem_fun_ref_t(_Ret (_Tp::*__pf)()) : _M_f(__pf) 12 { 13 } 14 15 _Ret operator()(_Tp& __r) const 16 { 17 return (__r.*_M_f)(); 18 } 19 20 private: 21 _Ret (_Tp::*_M_f)(); 22 }; 另一个mem_fun_ref可以转换一元类成员函数,之所以无法实现二元类成员函数的转换,可以从上面的源代码中看出,binary_function和unary_function最多只提供三个模板参数,而对于类成员函数来说,需要类类型、函数返回值,那么最多就只能有一个函数参数了。 mem_fun_ref(&A::cppPrint)的调用可以类比于ptr_fun(&cPrint),需要说明的是,for_each需要将*__first转换为class A实例,所以A必须提供不带explicit关键字的构造函数,以允许此隐式转换。 |
那么根据这几个adaptor,我们还可以做点儿大的。
4.2. 适配再适配…
SGI STL提供了三种强有力了的再适配器(bind、negate、compose),
bind |
bind1st:绑定第一个参数为非容器元素 |
bind2nd:绑定第二个参数为非容器元素 |
|
negate |
not1:对一元functor结果取非 |
not2:对二元functor结果取非 |
|
compose |
compose1:合成两个输入的functor |
compose2:合成三个输入的functor |
我们用侯捷老师的《STL源代码剖析》中的例子,来看下这些adaptor有多么强悍!
我们希望将容器v内的每一个元素element进行(element+2)*3,可以令f(x) = x*3,g(y) = y+2,那么一个富有诗意的表达式可以解决这个问题,
compose1(bind2nd(multiplies<int>(), 3), bind2nd(plus<int>(), 2));
生命不息,源码不止,我们还是来看bind2nd和compose1的源代码吧!
bind2nd的定义,
1 template<typename _Operation, typename _Tp> 2 inline binder2nd<_Operation> bind2nd(const _Operation& __fn, const _Tp& __x) 3 { 4 typedef typename _Operation::second_argument_type _Arg2_type; 5 return binder2nd<_Operation>(__fn, _Arg2_type(__x)); 6 } 7 8 template<typename _Operation> 9 class binder2nd : public unary_function<typename _Operation::first_argument_type, typename _Operation::result_type> 10 { 11 protected: 12 _Operation op; 13 typename _Operation::second_argument_type value; 14 15 public: 16 binder2nd(const _Operation& __x, const typename _Operation::second_arguement_type& __y) : op(__x), value(__y) 17 { 18 // 将原始仿函数op和第二个参数作为内部参数,保持此状态! 19 } 20 21 typename _Operation::result_type operator()(const typename _Operation::first_argument_type& __x) const 22 { 23 return op(__x, value); // 将value绑定为第二个参数 24 } 25 };
compose1的定义,
1 template <class _Operation1, class _Operation2> 2 inline unary_compose<_Operation1, _Operation2> compose1(const _Operation1& __fn1, const _Operation2& __fn2) 3 { 4 return unary_compose<_Operation1, _Operation2>(__fn1, __fn2); 5 } 6 7 template<class _Operation1, class _Operation2> 8 class unary_compose : public unary_function<typename, _Operation2::argument_type, typename _Operation1::result_type> 9 { 10 protected: 11 _Operation1 _M_fn1; 12 _Operation2 _M_fn2; 13 14 public: 15 unary_compose(const _Operation1& __x, const _Operation2& __y) : _M_fn1(__x), _M_fn2(__y) 16 { 17 // 将两个functor保存为内部成员 18 } 19 20 typename _Operation1::result_type operator()(const typename _Operation2::argument_type& __x) const 21 { 22 return _M_fn1(_M_fn2(__x)); // 很带艺术感的函数合成 23 } 24 }
对于上面的例子,目前我还处于只可意会、无法表达的阶段,可能离完全吃透GP编程还有很长的路。但是从上面的源代码,尤其是compose1,我们可以从中学习如何开发适合自己的、功能更复杂的adaptor,将代码变得更优雅。
『依然在路上』