标准模板库(STL)包含C++程序员不可或缺的许多东西。它还有力证明了C++的概念化编程能力。STL的概念包括容器(container)、范围(range)、算法(algorithm)以及仿函数(functor)。本文着重讲解仿函数,它本质上是一个类,但通过重载operator(),所以行为与函数相似。这个概念在STL之前便已存在,STL只是从另一个角度来看待它。继续阅读本文,你就能体会到个中三味。

 


STL以泛型方式来处理函数。假如一个参数的行为应该与函数相仿,STL算法就不关心它是一个实际的C++函数,还是一个仿函数。出于本文的目的,假定某个类有一个重载的operator(),而且重载的operator()要求获取一个参数,我们就将这个类称为“一元仿函数”(unary functor);相反,如果重载的operator()要求获取两个参数,就将这个类称为“二元仿函数”(binary functor)。

STL算法适用于范围。你可使用函数,并将它们应用于一个范围中的每个元素(参见清单A)。这样一来,就可以处理三种类型的函数:

 

  • 获取零个参数的函数,也称为“生成器”(generators),它们能生成范围。例如,假定一个函数能生成斐波那契数字,那么对该函数的每一个调用都能生成斐波那契数列中的下一个数字。
  • 获取一个参数的函数(一元函数)。
  • 获取两个参数的函数(二元函数)。

这其实已覆盖了大多数情况。极少数情况下,你要求函数获取3个或者3个以上的参数。在这种情况下,可考虑采取其他方式。例如,可将多个参数打包到一个结构中,再按引用传递它。


之所以要开发仿函数(functors),是因为函数不能容纳任何有意义的状态。例如,使用函数,你不能为某个元素加一个任意值,再将其应用于一个范围。但是,使用仿函数可轻易做到这一点,如清单B所示。

这演示了仿函数的一个主要优点——它们可以有背景(context)或状态。下面是使用仿函数时要记住的要点:

 

  • 仿函数以传值方式传给一个算法。
  • 每次只能应用一个仿函数,方法是为范围中的每个元素应用operator()。
  • 使用仿函数,可对范围中的每个函数做某事(比如为每个元素都乘以5),可基于整个范围来计算某个有意义的结果(比如求所有元素的平均值),或者同时进行这两种操作。
  • 对于一个给定的范围,仿函数不知道它要应用于多少个元素。

假定你要创建一个函数,要求它在给定一个范围的情况下,能为每个元素都返回当前已处理的所有元素的平均值。换言之:

  • 处理x1时,返回x1
  • 处理x2时,返回(x1 + x2) / 2
  • 处理x3时,返回(x1 + x2 + x3) / 3

清单C展示了怎样实现这个任务。

 

只要亲自编写和使用一下仿函数,就会体会到它具体如何降低复杂性。你不必关心整个范围,只需将注意力集中在一个元素上。这同时还有助于改善代码的可读性。清单D给出了示范性的generate_fibonacci代码。

 

 

前面讲述的都是一元仿函数。二元仿函数同等重要。二元仿函数同时应用于两个范围,或者应用于某个范围中的两个元素。二元仿函数的operator()要求获取两个参数,而不是一个。假定你有两个范围,分别有相同数量的元素,而你希望构建一个新的范围,比如:

  • 第一个元素:x1 * y1
  • 第二个元素:- x2 * y2
  • 第三个元素:x3 * y3
  • 第四个元素:- x4 * y4,等等。

清单E给出了一个示范性的实现。

 

为什么需要判断式


“判断式”(predicates)是仿函数的特例。事实上,你要写的许多仿函数都是判断式。假如一个仿函数返回的值能转换成bool类型(可为true或false),这个仿函数就是判断式。一元判断式(获取一个参数)能实现“筛选”,如清单F所示。

二元判断式能实现“比较相等性”和“排序”(比较两个值,判断一个是否小于另一个)。清单G展示了怎样比较两个范围的“近似”相等性。

不要低估判断式的重要性。下一次写代码时,注意一下你会在筛选、比较相等性以及排序上花费多少时间。使用判断式,不仅能节省大量时间,还能使编码工作更加轻松惬意。除此之外,代码还会变得更容易理解。

使用绑定仿函数

仿函数和判断式的真正优势反映在它们与binder组合使用的时候。binder允许为二元仿函数或判断式绑定一个值,从而将那个值固定下来。你可以绑定第一个或者第二个参数。随即,二元仿函数会变成一元仿函数。比如:

  • f = std::bind1st( functor, v); 'f( x)'等价于'functor( v, x)'
  • f = std::bind2nd( functor, v); 'f( x)'等价于'functor( x, v)'

你可以绑定一个二元仿函数,获得一个一元仿函数,再把它应用于一个范围。例如,假定我们要在一个范围中找出小于10的所有元素清单H展示了具体怎样做。

 

清单I所示,如果综合运用binder、仿函数和算法,就能获得多个方面的好处,包括:

  • 可以只打印小于一个给定值的元素。
  • 可以对范围进行分区,一个分区包含小于或等于一个值的元素,另一个分区则包含不小于那个值的元素。
  • 可以使范围中的所有元素都乘以一个值。
  • 可以移除大于一个给定值的所有元素。
  • 可以替换大于或等于一个值的所有元素。

STL配套提供了大量预定义的仿函数和判断式,包括std::lessstd::greaterstd::plusstd::minus,它们都在<functional>标头中。

 

仿函数定义

仿函数,又或叫做函数对象,是STL(标准模板库)六大组件(容器、配置器、迭代器、算法、配接器、仿函数)之一;仿函数虽然小,但却极大的拓展了算法的功能,几乎所有的算法都有仿函数版本。例如,查找算法find_if就是对find算法的扩展,标准的查找是两个元素向等就找到了,但是什么是相等在不同情况下却需要不同的定义,如地址相等,地址和邮编都相等,虽然这些相等的定义在变,但算法本身却不需要改变,这都多亏了仿函数。

仿函数之所以叫做函数对象,是因为仿函数都是定义了()函数运算操作符的类。例如,STL自带的仿函数equal_to<class Tp>定义为:

template <class _Tp>

struct equal_to : public binary_function<_Tp,_Tp,bool>

{

  bool operator()(const _Tp& __x, const _Tp& __y) const { return __x == __y; }

};

在算法内部调用此操作符,如find_if

template <class _RandomAccessIter, class _Predicate>

_STLP_INLINE_LOOP _RandomAccessIter __find_if(_RandomAccessIter __first, _RandomAccessIter __last,

                                              _Predicate __pred,

                                              const random_access_iterator_tag &)

{

  _STLP_DIFFERENCE_TYPE(_RandomAccessIter) __trip_count = (__last - __first) >> 2;

 

  for ( ; __trip_count > 0 ; --__trip_count) {

    if (__pred(*__first)) return __first;

++__first;

//以下略

}

仿函数的可配接性

仿函数的可配接性是指仿函数能够与其它仿函数配接在一起实现新的功能,如不小于60,可以利用STL自带的not1<int>less<int>配接而成:not1(bind2nd(less<int>(), 12))

一般而言,通用函数也可以作为仿函数参数传递给算法,但其区别在于“通用函数不具有可配接性”。是否定义成仿函数都具有配接性了呢?也不尽然!只有从unary_function或者binary_funcion继承的仿函数才有配接性。这是为什么呢?

其奥妙在于模板类常见的类型定义,可配接性的关键就在于这些类型定义;如binary_function

template <class _Arg1, class _Arg2, class _Result>

struct binary_function {

  typedef _Arg1 first_argument_type;

  typedef _Arg2 second_argument_type;

  typedef _Result result_type;

}; 

STL的适配器中会自动使用到这些类型定义,所以必须声明这些类型。

把通用函数转换为仿函数

STL的实现也考虑到会将通用函数作为仿函数来使用,为了保证这些函数的可配接性,即把这些函数转换为仿函数使用,STL也提供了相应的适配器ptr_fun1_baseptr_fun2_base,其原理也是重载函数调用操作符,在仿函数对象构造时把通用函数作为参数传入,如:

template <class _Arg, class _Result>

class pointer_to_unary_function : public unary_function<_Arg, _Result>

{

protected:

   //函数原型

  _Result (*_M_ptr)(_Arg);

public:

  pointer_to_unary_function() {}

  //构造时把函数指针传入

  explicit pointer_to_unary_function(_Result (*__x)(_Arg)) : _M_ptr(__x) {}

  //()函数运算操作符重载,执行函数功能

  _Result operator()(_Arg __x) const { return _M_ptr(__x); }

};

把类成员函数转换为仿函数

既然通用函数都能转换为仿函数,带有C++封装性的类的成员函数(当然要是public)也能否转换为仿函数?答案是肯定的,STL也提供了相应适配器。由于返回值和参数的个数不同,这类适配器的数目很多:_Void_mem_fun0_ptr_Void_mem_fun1_ptr_Void_const_mem_fun0_ptr_Void_const_mem_fun1_ptr等。

例子中使用通用函数和成员函数作为仿函数配合STL算法使用。

class Numbers

{

public:    

    //用于显示

       bool display()

       {

              cout << *this;

              return true;

       }

       //用于查找

       bool if_equal(int val)

       {

              return val == m_val;

       }

};

如下的语句验证了ptr_fun转换后的仿函数的可配接性:

vector<int>::iterator it = find_if(vNums.begin(), vNums.end(), bind2nd(ptr_fun(if_equal), val));

for_each(vObjs.begin(), vObjs.end(), mem_fun(&Numbers::display));vector<Numbers*>::iterator itObj=find_if(vObjs.begin(), vObjs.end(), bind2nd(mem_fun1(&Numbers::if_equal), 3));说明了如何使用STL的适配器来转换类成员函数。需要说明的是,在转换成员函数时,有引用和指针两个版本,例子程序中使用的是指针版本,所以定义vector时定义元素类型尾Number*。这是因为这时适配器的函数操作符是通过指针形式调用的,如mem_fun1返回mem_fun1_t的内部实现为:

Ret operator()(_Tp* __p, _Arg __x) const { return (__p->*_M_f)(__x); }

定义自己的仿函数类型

上面说过定义可配接的仿函数,只需要从unary_functionbinary_function派生即可,但是STL只定义了这两种类型;但我们有可能需要使用3个参数的仿函数,同时也更能体会可配接性的原理,这里给出了triple_function的函数原型,可以STL的作为一种扩展。

//用于方便提前类的类型定义

#define TRIPLE_ARG(Operation, Type) Operation::Type

//三元函数的类型定义

template<class Arg1, class Arg2, class Arg3, class Result>

struct triple_funcion

{

    //保证可配接性的类型定义

       typedef Arg1 first_argument_type;

       typedef Arg2 second_argument_type;

       typedef Arg3 third_argument_type;

       typedef Result result_type;

};

 

//三元函数的适配器,把第3个参数固定为特定值

template <class Operation>

class binder3rd : public binary_function<typename TRIPLE_ARG(Operation, first_argument_type),

typename TRIPLE_ARG(Operation, second_argument_type), typename TRIPLE_ARG(Operation, result_type)>

{

protected:

       Operation m_op;

       typename Operation::third_argument_type  value;

public:

       binder3rd(const Operation& x, const typename Operation::third_argument_type y):m_op(x), value(y){}

    //通过固定第三个参数,把函数转换为binary_function

       typename Operation::result_type operator()(const Operation::first_argument_type& x,

              const Operation::second_argument_type& y) const

       {

              return m_op(x, y, value);

       }

      

};

 

//上层使用的包装类

template<class Operation, class Arg>

inline binder3rd<Operation> bind3rd(const Operation& fn, const Arg& x)

{

       typedef Operation::third_argument_type third_argument_type;

       return binder3rd<Operation>(fn, third_argument_type(x));

}

 

在例子中定义了一个三元仿函数:

class Qualified : public triple_funcion<Student, int, int, bool>

{

public:

       bool operator()(const Student& s, int math, int physics) const

       {

              return s.math > math && s.physics > physics;

       }

};

用于查找数学和物理两科成绩符合条件的学生。

查找时,通过bind3rdbind2nd把数学和物理的成绩基线定下来:数学>40,物理>60

it = find_if(it, students.end(), bind2nd(bind3rd(Qualified(), 40), 60));

 

小结

仿函数小巧和作用大,原因是其可配接性和用于算法;可以根据需要把相关函数封装到类中,或者调用基本的函数库来减少开发量。只要知道了 STL 适配器内部机制,就能定义出符合要求的仿函数来。

仿函数:用途和适用的场合

算法、范围和函数
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐