STL源码学习----函数对象(转)

2013-12-1 admin 转载

STL中很多算法都要对迭代器范围内的元素做特定操作,这些操作是需要用户显示传递给迭代器,如何才能有效地传递这些操作呢?STL为我们提供了函数对象来解决这个问题。本文先简要介绍函数对象的概念,然后配合源代码介绍STL为我们提供的几种函数对象,最后介绍强大的函数对象适配器。

 

0 函数对象

标准库的很多算法需要一些指向序列的迭代器对该序列作操作,算法的参数除了迭代器以外还会需要用户指定一些值或指定一些操作,以应用到迭代器指向的元素上。

例如要用6替代某vector中的所有值为3的元素:

1 Void func(vector<int>& c)
2 {
3 replace(c.begin(), c.end(), 3, 6);
4 }

但是,有时候,我们需要替代序列中满足某些条件的元素,就像下面一样:

1 bool bigthan5(int v)
2 {
3     return v>5;
4 }
5 replace_if(vec.begin(), vec.end(), bigthan5, 8);

像上面这样,存在很多为算法传递函数(如谓词演算,逻辑运算等)的情形,如果每次都需要用手手动去编写传递给算法的函数既不方便也不有效。相对来说,类的成员函数能更好地提供这种服务,因为类的对象可以保存数据,而且,类还能作一些初始化数据的工作。

例如对于上面的条件替换,我们可以像下面这样用:

1 template<class T>
2 class Bigthan5
3 {
4 public:
5         bool operator()(T& v){return v>5;}
6 };
7
8 Bigthan5<int> s;
9 repace_if(vec.begin(), vec.end(), s, 8); //将vec中所有值大于5的元素替换为8

在上面的replace_if()中,对于vec.begin()和vec.end()范围内的所有元素都调用了函数Bigthan5<int>::operator()(int)函数。

上面的类Bigthan5重载了opearator()操作符。运算符()的典型应用是为对象提供常规的函数调用的语法形式,使他们具有像函数一样的行为方式,一个应用起来像函数的对象被称作一个拟函数对象,简称函数对象

《C++程序设计语言》中指出:一个函数对象的开销比指针传递函数的开销小,所以函数对象通常比常规函数执行速度更快。

 

标准库为我们提供了丰富的函数对象

1,函数对象的基类

标准库提供了两种函数对象基类:

 1 template <class _Arg, class _Result>
 2 struct unary_function {
 3   typedef _Arg argument_type;
 4   typedef _Result result_type;
 5 };
 6
 7 template <class _Arg1, class _Arg2, class _Result>
 8 struct binary_function {
 9   typedef _Arg1 first_argument_type;
10   typedef _Arg2 second_argument_type;
11   typedef _Result result_type;
12 };

其中,类unary_function应用于一元操作符,它会接受两个模板参数,一个作为其返回值类型,另一个作为其参数类型;相应地,binary_function应用于二元操作符。

 

2,算术函数对象及其应用

STL提供了几种算术函数对象,分别是:plus, minus, mutiplies, divides, modulus, negate。它们长得都差不多,类似于下面的样子:

1 template <class _Tp>
2 struct divides : public binary_function<_Tp,_Tp,_Tp> {
3   _Tp operator()(const _Tp& __x, const _Tp& __y) const { return __x / __y; }
4 };

需要注意的是,这些算术函数对象的参数类型和返回值类型是一样的,这个不难理解。

我们可以像下面这样用算术函数对象:

int result = accumulate(vec.begin(), vec.end(), 1, mutiplies<int>());

该函数将vec.begin()和vec.end()之间的元素累积相乘。

 

3,谓词函数对象及其应用

STL提供了一下几种谓词函数对象: equal_to, not_equal_to, greater, less, greater_equal, less_equal, logical_and, logical_or, logical_not。它们长得都类似于下面的样子:

1 template <class _Tp>
2 struct logical_and : public binary_function<_Tp,_Tp,bool>
3 {
4   bool operator()(const _Tp& __x, const _Tp& __y) const { return __x && __y; }
5 };

谓词函数对象的返回值都是bool类型。

我们可以像下面这样用谓词函数对象:

1 sort(vec.begin(), vec.end(), less<int>());

上面的语句表示将序列vec的所有元素递增排序。

 

4,用户自定义函数对象

标准库提供的函数对象的最大灵活性在于用户可以自定义函数对象,当我们把算法应用于那些在设计时并没有考虑标准库和标准算法的类时,自定义函数对象的能力就特别重要了[1]

《C++程序设计语言》中提供了用户自定义谓词函数对象的一个例子:

 1 class Person {...};
 2 struct Club{
 3 string name;
 4 list<Person*> members;
 5 List<Person*> officers;
 6 //…
 7 Club(const string& s);
 8 };
 9
10 Class Club_eq : public unary_function<Club, bool>{
11 String s;
12 public:
13 explict Club_eq(const string& ss) : s(ss) {}
14 bool operator()(const Club& c) const {return c.name == s;}
15 };
16 void f(list<Club>& lc)
17 {
18 typedef list<Club>::iterator LCI;
19 LCI p = find_If(lc.begin(), lc.end(), Club_eq("Dining Philosophers"));
20 }

 

一般情况下,在使用函数对象的时候,向STL算法传递一个对象即可,算法会自动调用该对象的operator()操作符。

 

5,函数对象的适配器

 

STL为支持标准函数对象的组合提供了标准函数对象的适配器。

这些适配器都有共同的特征:它们都依赖于函数对象基类unary_function, binary_function, 对这些适配器中的每一个都提供了一个协助函数,它以一个函数对象为参数,返回另一个合适的函数对象。也就是说,这种适配器是一种形式简单的高阶函数,它以一个函数作为参数并具此产生另一个新的函数[1]

 5.1约束器

bind1st和bind2nd可以绑定二元函数对象的某一个值。值得注意的是:bind1st和bind2nd不是函数对象,它们是普通的函数,它们的输入参数中,第一个参数是二元函数对象,第二个参数是要绑定的值(对于bind1st是绑定二元函数参数的第一个参数,对于bind2nd是绑定二元函数参数的第二个参数)。

下面是bind1st的完整实现代码:

 1 template <class _Operation>
 2 class binder1st
 3   : public unary_function<typename _Operation::second_argument_type,
 4                           typename _Operation::result_type> {
 5 protected:
 6   _Operation op;
 7   typename _Operation::first_argument_type value;
 8 public:
 9   binder1st(const _Operation& __x,
10             const typename _Operation::first_argument_type& __y)
11       : op(__x), value(__y) {}
12   typename _Operation::result_type
13   operator()(const typename _Operation::second_argument_type& __x) const {
14     return op(value, __x);
15   }
16 };
17
18 template <class _Operation, class _Tp>
19 inline binder1st<_Operation>
20 bind1st(const _Operation& __fn, const _Tp& __x)
21 {
22   typedef typename _Operation::first_argument_type _Arg1_type;
23   return binder1st<_Operation>(__fn, _Arg1_type(__x));
24 }

在我们最上面提供的替换大于5的元素的实现中,我们可以像下面的方式这样调用bind1st:

1 #include <functional>
2 ...
3 replace_if(vec.begin(), vec.end(), bind1st(less<int>(), 5), 8);

 

5.2 成员函数适配器

 

有时候,用户要给某个算法传递一个对象的成员函数,这个时候,我们需要用到成员函数的适配器mem_fun()和mem_fun_ref()。

其中,mem_fun()接受一个对象指针传递过来的成员函数,mem_fun_ref()接受一个对象引用传递过来的成员函数,它们都返回一个函数对象。

下面摘录一部分mem_fun()的实现代码:

 

 1 //这个版本的mem_fun接受一个指针、无参、非void返回值、非const成员函数
 2 // _Tp::*f 表示指向类_Tp成员函数的指针
 3 template <class _Ret, class _Tp>
 4 inline mem_fun_t<_Ret,_Tp> mem_fun(_Ret (_Tp::*__f)())
 5   { return mem_fun_t<_Ret,_Tp>(__f); }
 6
 7 template <class _Ret, class _Tp>
 8 class mem_fun_t : public unary_function<_Tp*,_Ret> {
 9 public:
10   explicit mem_fun_t(_Ret (_Tp::*__pf)()) : _M_f(__pf) {}
11   _Ret operator()(_Tp* __p) const { return (__p->*_M_f)(); }
12 private:
13   _Ret (_Tp::*_M_f)();
14 };

我们可以像下面这样使用mem_fun():

1 void draw_all(list<Shape*>& lsp){
2 For_each(lsp.begin(), lsp.end(), mem_fun(&Shape::draw));
3 }

mem_fun_t()和mem_fun_ref_t()函数族群共有16 = 2 ^ 4个函数,分别对应:

1)成员函数无参数 or 成员函数有一个参数

2)通过指针调用 or 通过引用调用

3)无返回值 or 有返回值

4)const成员函数 or non_const成员函数

 

 5.3普通函数适配器

 

 

与5.2类似,有时候用户需要给算法传递一个普通函数,这个时候,我们需要用到普通函数的适配器ptr_fun(),它有两个重载版本,一个有一个参数,另一个有两个参数。

适配器ptr_fun()返回一个函数对象

下面的代码是拥有一个参数的普通函数适配器的实现代码:

 1 template <class _Arg, class _Result>
 2 class pointer_to_unary_function : public unary_function<_Arg, _Result> {
 3 protected:
 4   _Result (*_M_ptr)(_Arg);
 5 public:
 6   pointer_to_unary_function() {}
 7   explicit pointer_to_unary_function(_Result (*__x)(_Arg)) : _M_ptr(__x) {}
 8   _Result operator()(_Arg __x) const { return _M_ptr(__x); }
 9 };
10
11 template <class _Arg, class _Result>
12 inline pointer_to_unary_function<_Arg, _Result>
13 ptr_fun(_Result (*__x)(_Arg))
14 {
15   return pointer_to_unary_function<_Arg, _Result>(__x);
16 }

我们可以看到,ptr_fun接受一个函数指针x,返回一个类pointer_to_unary_function的对象,该对象重载operator()操作符时调用x传递过来的函数。

 

在我们最上面提供的替换大于5的元素的实现中,我们可以像下面的方式这样调用bind1st:

#include <functional>
...
replace_if(vec.begin(), vec.end(), ptr_fun(bigthan5), 8);

 

5.4谓词否定迭代器

 

顾名思义,这个否定迭代器会接受任何一个返回值为bool类型的模板参数,这个模板参数可以是上面提到的任何返回值为bool的函数,也可以是用户自定义的返回值为bool的函数,或者返回值为bool的类成员函数。

对这个bool取反之后返回一个函数对象。

否定迭代器由not1和not2组成,它们分别有一个或两个模板参数。

下面是not2的实现代码:

 1 template <class _Predicate>
 2 class binary_negate
 3   : public binary_function<typename _Predicate::first_argument_type,
 4                            typename _Predicate::second_argument_type,
 5                            bool> {
 6 protected:
 7   _Predicate _M_pred;
 8 public:
 9   explicit binary_negate(const _Predicate& __x) : _M_pred(__x) {}
10   bool operator()(const typename _Predicate::first_argument_type& __x,
11                   const typename _Predicate::second_argument_type& __y) const
12   {
13     return !_M_pred(__x, __y);
14   }
15 };
16
17 template <class _Predicate>
18 inline binary_negate<_Predicate>
19 not2(const _Predicate& __pred)
20 {
21   return binary_negate<_Predicate>(__pred);
22 }

在我们最上面提供的替换元素的实现中,我们可以像下面的方式这样调用bind1st:

1 #include <functional>
2 ...
3 replace_if(vec.begin(), vec.end(), not1(ptr_fun(bigthan5)), 8);

上面的代码将所有小于等于5的元素替换为8。

 

6 总结

函数对象在STL的算法部分占有很重要的作用,STL中基本上所有的算法都像__pred(*__first)或__binary_op(*__first1, *__first2)这样对迭代器指向的元素进行操作,这就需要用户正确地传递__pred和__binary_op给算法。本节中介绍的函数对象不论在封装性还是性能上都完全胜任这个任务。

对函数对象的几种类型有明确的理解,并辅之以相关的练习,定会掌握它。

 

7 参考书目

[1] 《C++程序设计语言》 Bjarne Stroustrip

[2] SGI STL-3.3源代码

 

前三节简要介绍了STL中的内存管理、迭代器和函数对象,从下一节开始,我们将踏上美丽的容器和算法之旅。

 

原文:http://www.cnblogs.com/cobbliu/archive/2012/04/21/2461184.html

标签: C++

发表评论:

Powered by emlog

浙ICP备17021512号 |浙公网安备 33010602008237号