仿函数 函数适配器
仿函数(函数对象)和适配器可以说是STL中默默无闻的贡献者,它们没有容器、算法和迭代器那么有名, 但是其贡献却很大。
一、仿函数
仿函数又称函数对象,从名字上可以得出,它本质上是 一种具有函数特质的对象, 也即可以像使用函数一样使用该对象。怎么样做?重载operator()运算符即可,有了这个运算符,我们就可以在仿函数对象后
面加上一对小括号,以此调用仿函数所定义的operator()。
STL仿函数可以分为一元和二元,或者算术运算、关系运算和逻辑运算。
二、为什么要有仿函数?
在算法的设计过程中,我们会发现其本质往往是不变的(例如排序算法的思想),变化的除了数据之外还有操作(例如排序中不一定是比较大小,也可以是两两之间满足某种关系),仿函数就是为了这种情况产生的,它替代原来需要函数指针的地方,把这种操作或者策略传给算法,使得算法抽象性更高,也就更通用。
为什么不用函数指针?
很简单的解释是抽象性不够,更进一步说是它无法配接,也就是可以将操作配接在一起变换为更复杂的操作(例如compose和bind1st等等方法),仿函数则可以轻松实现这些配接,使得其功能异常强大。
三、仿函数实现
仿函数在实现上是一个结构体,并且如上所述重载了operator()运算符,所有的仿函数如果是一元的都继承自unary_function,二元则继承自binary_function,因为继承自这两个函数的仿函数均定义了相应型别供配接时使用,也就具有了配接能力。
template <class Arg, class Result>
struct unary_function {
typedef Arg argument_type;
typedef Result result_type;
};
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内置的仿函数则仅仅是重载了()运算,很简单:
加法是二元运算符所以继承自binary_function
template <class T>
struct plus : public binary_function<T, T, T> {
T operator()(const T& x, const T& y) const { return x + y; }
};
非是一元逻辑运算符,所以继承自unary_function
template <class T>
struct logical_not : public unary_function<T, bool> {
bool operator()(const T& x) const { return !x; }
};
四、函数适配器
适配器也是一种常用的设计模式: 将一个class的接口转换为另一个class的接口,使得原本因接口不兼容而不能合作的classes可以一起运作。
一个通俗的例子是我们笔记本的电源,一般都会有一个适配器把220v的电压降到适合笔记本工作的电压范围,这样笔记本就可以工作在我们常用的电压环境了,这就扩大了笔记本的使用场景,在软件开发过程中也是一样的道理。
STL提供三种适配器:改变容器接口的容器适配器、改变迭代器接口的迭代器适配器以及改变仿函数接口的仿函数适配器。前两者都较为简单,而最后一种则是灵活性最大的,有了它我们可以构造非常复杂的表达式策略。
1. 容器适配器
容器适配器常见的是stack和queue,他们的底层存储都是用deque完成的,再在deque上封装一层接口以满足stack和queue的要求。
2. 迭代器适配器
迭代器适配器大致有三种对应不同的迭代器行为,它们以某容器为参数,直接对容器的迭代器进行封装,主要有back_insert_iterator、front_insert_iterator、insert_iterator以及reverse_iterator。
3. 仿函数适配器
从上面两个适配器看,其原理都是在其内部有一个原来要适配的成员变量,通过改变接口来实现,那么仿函数的适配器也不例外。常用的是bind1st,bind2nd,not1,compose1,compose2等等,这些适配器都是仿函数,同时以要适配的仿函数作为member object。
仿函数适配器的实现主要包括两块,自身的类以及方便使用的函数,以bind1st为例,它的作用是绑定二元仿函数的第一个参数为某指定值。首先是其定义:
从它继承自unary_function即可得知它也是仿函数
template <class Operation>
class binder1st:
public unary_function<typename Operation::second_argument_type,
typename Operation::result_type>
{
protected:
Operation op; //以要适配的仿函数为成员变量
typename Operation::first_argument_type value; //第一个参数
public:
binder1st(const Operation& x,
const typename Operation::first_argument_type& y)
: op(x), value(y) {} //构造函数里对两个成员变量赋值
typename Operation::result_type
operator()(const typename Operation::second_argument_type
&x) const
{
return op(value, x); //重载并接受第二个参数,以完成适配
}
};
仿函数适配器第二个部分是方便使用的函数,以让我们可以像普通函数一样使用适配器,并通过函数模板的参数推导功能来创建适配器对象。
template <class Operation, class T>
inline binder1st<Operation> bind1st(const Operation& op, const T& x) {
typedef typename Operation::first_argument_type arg1_type;
return binder1st<Operation>(op, arg1_type(x));//返回对象
}
适配器很巧妙的构造了这样一种对象嵌套对象的结构来使得我们可以构造很复杂的语义,这也是函数指针所不具备的,当然对于函数指针STL也提供了ptr_fun来将其变为函数对象以获得适配功能,成员函数得使用mem_fun,mem_fun_ref。
So, 函数适配器有也可分为两种:
1. 针对一般函数(非成员函数)而设计的函数适配器
2. 针对成员函数而设计的函数适配器
总的来说,通过对适配器和仿函数的学习我们看到了STL里面精妙的封装,以及对高度抽象的追求,也正是这些高度抽象使得它更加通用性。有了这些知识,我们可以利用系统的迭代器构造自己的迭代器,来扩展STL的功能
Reference
[2] 仿函数和仿函数适配器
[3] STL之仿函数,适配器简介 较为浅显,可以先看
[4] STL与泛型编程<十五>:预定义的仿函数和仿函数适配器
[5] 仿函数(函数对象)和STL算法