仿函数
仿函数又称函数对象,它本质上是 一种具有函数特质的对象,它 重载了operator()运算符,我们可以像使用函数一样使用该对象。
比如:
template <class T>
struct greater
{
bool operator()(const T &x, const T &y) const { return x > y; }
};
//方式1
greater<int> ig;
cout << ig(4, 6) << endl; //false
//方式2,使用临时对象
cout << greater<int>()(6, 4) << endl; //true
仿函数的作用:
举个例子,在使用sort的时候,我们想按照特定的规则进行排序,它允许我们传入一个仿函数,仿函数即指定了,某种排序策略。这样一来,使得算法更加灵活。
为什么不用函数指针:
- 函数指针不满足STL对抽象性的要求;
- 函数指针无法和STL其他组件搭配,产生更灵活的变化。
关于第二点,举个简单的例子:
我们想要找出“不小于”12的元素个数,可以这么做:
//仿函数bind2nd(less<int>, 12)找出小于12的元素
//仿函数not1进行取反得到“不小于”
not1(bind2nd(less<int>, 12));
STL仿函数可以分为一元和二元,或者算术运算、关系运算和逻辑运算。
仿函数在实现上是一个结构体,所有的一元仿函数都继承自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; }
};
配接器(adapters)
配接器(adapters) : 将一个class的接口转换为另一个class的接口,使得原本因接口不兼容而不能合作的classes可以一起运作。
举个生活中的例子:你的电脑充电器接口是三脚的,而墙上的插座接口是两脚的,两者不兼容,于是你可以使用一个排插,排插一端可以跟墙上的插座合作,另一端可以跟电脑的三脚接口合作,这个时候排插就是一个典型的配接器了。
STL提供三种适配器:改变容器接口的容器适配器、改变迭代器接口的迭代器适配器以及改变仿函数接口的仿函数适配器。
前两者都较为简单,而最后一种则是灵活性最大的,有了它我们可以构造非常复杂的表达式策略。
容器适配器常见的是stack和queue,他们的底层是用deque完成的,只需要在deque上封装一层接口以满足stack和queue的要求。
迭代器适配器大致有三种对应不同的迭代器行为,它们以某容器为参数,直接对容器的迭代器进行封装,主要有back_insert_iterator、front_insert_iterator、insert_iterator以及reverse_iterator。
举个例子,假设iter是一个reverse_iterator,则对于iter++,它等价于普通的iterator的iter–;也就是说,reverse_iterator改变了iterator的接口,使得我们可以换另一个方向,从“尾部开始”来做一些事情。
我们稍微总结一下:
stack和queue内部包含了一个deque;
reverse_iterator内部包含了一个iterator;
上面两种配接器都是在内部包含了一个原来要适配的成员变量,通过改变接口来实现配接。
仿函数也不例外,常用的有bind1st,bind2nd,not1,compose1,compose2等等,这些适配器都是仿函数,他们以要适配的仿函数作为成员变量。
仿函数适配器的实现主要包括两块,自身的类以及方便使用的函数,以bind1st为例,它的作用是绑定二元仿函数的第一个参数为某指定值。首先是其定义:
//将可配接的二元仿函数Operation转换为一元仿函数
//从它继承自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); //重载操作符(),并接受第二个参数类型的值,以完成适配
}
};
在构造binder1st的时候,使用一个要配接的仿函数,以及一个第一个参数类型的值来初始化,之后对于仿函数binder1st(),它只需要接受第二个参数类型的值,而第一个参数类型的值是绑定了的。
上面的仿函数在实际使用起来并不方便,我们可以定义以下辅助函数:
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。
STL精妙的封装,做到了高度抽象,使得它更加通用。