仿函数和配接器

先说一下STL提供的六大组件,彼此之间可以组合套用

1、容器(Containers):各种数据结构,如:vector、list、deque、set、map。用来存放数据。从实现的角度来看,STL容器是一种class template。

2、算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。

3、迭代器(iterators):容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。

4、仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。

5、配接器(adapters);一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。

6、配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。

这六大组件的交互关系:container(容器) 通过 allocator(配置器) 取得数据储存空间,algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,adapter(配接器) 可以修饰或套接 functor(仿函数)。

仿函数

仿函数的概念

仿函数也叫做函数对象,就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为(比如类A的对象a调用operator(),那么就是a(),是不是看起来特别像函数),就是一个仿函数类了。

仿函数的作用

STL所提供的各种算法,往往有两个版本,其中一个版本表现出最常用(或最直观)的某种运算,第二个版本则表现出最泛化的演算流程,允许用户“以template参数来指定所要采行的策略”。以sort()为例,其第二版本是以operator<为排序时的元素位置调整依据,第一版本则允许用户指定任何“操作”,务求排序后的两两相邻元素都能令该操作结果为true。

template<class _RanIt,
	class _Pr> inline
	void sort(_RanIt _First, _RanIt _Last, _Pr _Pred)
	{	// order [_First, _Last), using _Pred
	_DEBUG_RANGE(_First, _Last);
	_DEBUG_POINTER_IF(2 <= _Last - _First, _Pred);
	_Sort(_Unchecked(_First), _Unchecked(_Last), _Last - _First, _Pred);
	}

		// TEMPLATE FUNCTION sort
template<class _RanIt> inline
	void sort(_RanIt _First, _RanIt _Last)
	{	// order [_First, _Last), using operator<
	_STD sort(_First, _Last, less<>());
	}

要将某种“操作”当做算法的参数,唯一办法就是先将该“操作”(可能拥有数条以上的指令)设计为一个函数,再将函数指针当做算法的一个参数;或是将该“操作”设计为一个所谓的仿函数(就语言层面来说是个class),再以该仿函数产生一个对象,并以此对象作为算法的一个参数。

根据以上陈述,既然函数指针可以达到“将整组操作当做算法的参数”,那又何必有所谓的仿函数呢?原因在于函数指针毕竟不能满足STL对抽象性的要求,也不能满足软件积木的要求——函数指针无法和STL其他组件(如配接器adapter)搭配,产生更灵活的变化。

仿函数的分类

STL 仿函数的分类,若以操作数的个数划分,可分为一元和二元仿函数;若以功能划分,可分为算术运算、关系运算、逻辑运算三大类。

算术类仿函数

以+为例,此外还有-、*、?、%、-(否定)

template<class T>
struct plus :public binary_function<T, T, T> {
	T operator()(const T& x, const T& y) const { return x + y; }
};

关系类仿函数

以=为例,此外还有!=、>、>=、<、<=

template<class T>
struct equal_to :public binary_function<T, T, bool> {
	bool operator()(const T& x, const T& y) const { return x == y; }
};

逻辑类仿函数

以And为例,此外还有Or、Not

template<class T>
struct logical_and :public binary_function<T, T, bool> {
	bool operator()(const T& x, const T& y) const { return x && y; }
};

仿函数的可配接能力

STL仿函数应该有能力被函数配接器修饰,彼此像积木一样地串接。为了拥有配接能力,每一个仿函数必须定义自己的相应型别,就像迭代器如果要融入整个STL大家庭,也必须依照规定定义自己的5个相应型别一样。这些相应型别是为了让配接器能够取出,获得仿函数的某些信息(仿函数能够保存信息,函数指针则不能)。相应型别都只是一些typedef,所有必要操作在编译期就全部完成了,对程序的执行效率没有任何影响,不带来任何额外负担。

仿函数的相应型别主要用来表现函数参数型别和传回值型别。为了方便起见,<stl_function.h>定义了两个classes,分别代表一元仿函数和二元仿函数(STL不支持三元仿函数),其中没有任何data members或member functions,唯有一些型别定义。任何仿函数,只要依个人需求选择继承其中一个class,便自动拥有了那些相应型别,也就自动拥有了配接能力。

1. unary_function

unary_function 用来呈现一元函数的参数型别和返回值型别。其定义非常简单:

// STL 规定,每一个Adaptable Unary Function 都应该继承此类别
template <class Arg, class Result>
struct unary_function
{
    typedef Arg argument_type;
    typedef Result result_type;
};

一旦某个仿函数继承了uanry_function,其用户便可以取得该仿函数的参数型别,并以相同手法取得其返回值型别;

// 以下仿函数继承了uanry_function。
template <class T>
struct negate:public uanry_function<T,T>
{
    T operator() (const T& x) const { return -x; };
};

// 以下配接器(adapter)用来表示某个仿函数的逻辑负值
template <class Predicate>
class unary_negate
    ...
public:    // 模板中,需要typename来指明后面的定义是一个类型
    bool operator() (const typename Predicate::argument_type& x) const 
    {
        ....
    }
};

 

2. binary_function

binary_function用来呈现二元函数的第一参数型别、第二参数型别,以及返回值型别。其定义非常简单:

// STL规定,每一个Adaptable 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;
};

一旦某个仿函数继承了binary_function,其用户便可以这样取得该仿函数的各种型别:

// 以下仿函数继承了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; };
};

// 以下配接器(adapter)用来将某个二元仿函数转化为一元仿函数
template <class Operation>
class binder1st
    ....
protected:
    Operation op;
    typename Operation::first_argument_type value;
public:
    // 注意,这里的返回值和参数,都需要加上typename,告诉编译器其为一个类型值
    typename Operation::result_type operator() (const typename Operation::second_argument_type& x) const { ... }
};

 

配接器

《Design Patterns》一书提到23个最普及的设计模式,其中对adapter样式的定义如下:将一个class的接口转换为另一个class 的接口,使原本因接口不兼容而不能合作的classes,可以一起运作。

1. 配接器概观与分类

STL 所提供的各种配接器中,改变仿函数接口者,我们成为function adapter;改变容器接口者,我们称为container adapter;改变迭代器接口者,我们称为iterator adapter。

1.1 应用于容器——container adapter

STL提供的两个容器queue和stack,其实都只不过是一种配接器,它们修饰deque 的接口而成就出另一种容器风貌。

1.2 应用于迭代器——iterator adapter

STL提供了许多应用于迭代器身上的配接器,包括insert iterators, reverse iterators, iostream iterators。C++ Standard 规定它们的接口可以藉由<iterator> 获得,SGI STL 则将它们实际定义于<stl_iterator.h>.

(1)Insert Iterators

所谓Insert Iterators,可以将一般迭代器的赋值操作转变为插入操作。这样的迭代器包括专司尾端插入操作的back_insert_iterator,专司头端插入操作的front_insert_iterator,以及可从任意位置执行插入操作的insert_iterator。由于这三个iterator adapters 的使用接口不是十分直观,给一般用户带来困扰,因此,STL 更提供了三个相应函数:back_inserter(), front_inserter(), inserter(), 提升使用时的便利性。

(2)Reverse Iterators

所谓reverse iterators ,可以将一般迭代器的行进方向逆转,使原本应该前进的operator++变成后退操作,使原本应该后退的operator--变成前进操作。

(3)IOStream Iterators

所谓iostream iterators 可以将迭代器绑定到某个iostream对象身上。例如,绑定到istream对象(如std::cin)身上的,称为istream_iterator,拥有输入功能;也可以绑定到ostream对象身上。

#include<iostream>
#include<vector>
#include<string>
#include<iterator>
using namespace std;

int main()
{
	vector<int> v = { 1,2,3,4,5 };
	ostream_iterator<int> outite(cout, " ");
	copy(v.begin(), v.end(), outite);
	cout << endl;
	vector<int> v2 = { 8,9 };
	copy(v2.begin(), v2.end(), back_inserter(v));
	copy(v.begin(), v.end(), outite);
	cout << endl;
	system("pause");
	return 0;
}

 

1.3 应用于仿函数——functor adapters

functor adapters,或叫做function adapters,是所有配接器中数量最庞大的一个族群,其配接灵活度也是前二者所不能及,可以配接,配接,再配接。这些配接操作包括绑定(bind),否定(negate),组合(compose),以及对一般函数或成员函数的修饰(使其成为一个仿函数)。C++ Standard 规定这些配接器的接口可由<functional>获得,SGI STL 则将它们实际定义于<stl_function.h>。

function adapters的价值在于,通过它们之间的绑定、组合、修饰能力,几乎可以无限制地创造出各种可能的表达式,搭配STL算法一起演出。

各种function adapters及其辅助函数

综合应用举例

#include <iostream>
#include <iterator>
#include <vector>
#include <functional>
#include <algorithm>
using namespace std;
//这个一般函数待会会被当作仿函数使用
void print(int i)
{
	cout << i << ' ';
}

class Int
{
public:
	explicit Int(int i) :m_i(i) {}
	//这个成员函数待会会被当作仿函数使用
	void print1()const { cout << '[' << m_i << ']'; }
private:
	int m_i;
};
int main()
{
	//将outite绑定到cout,每次对outite指派一个元素,就后接一个" ",然后输出
	ostream_iterator<int> outite(cout, " ");
	int ia[6] = { 1,2,3,4,5,6 };
	vector<int> iv(ia, ia + 6);
	/*
	找出不小于12的元素个数
	count_if是算法
	bind2nd()和not1()是配接器,bind2nd()用于将3绑定到less算法的第二个参数
	not1()用于对运算结果加上否定运算
	*/
	cout << count_if(iv.begin(), iv.end(), not1(bind2nd(less<int>(), 3)));
	cout << endl;
	copy(iv.begin(), iv.end(), outite);
	cout << endl;
	for_each(iv.begin(), iv.end(), print);
	cout << endl;
	//以修饰过的一元函数搭配STL算法
	for_each(iv.begin(), iv.end(), ptr_fun(print));
	cout << endl;
	//成员函数指针系列的迭代器,mem_fun_ref()中的类型必须和
	//容器中的类型一样,去看mem_fun_red()的源码,是由同一个类的
	//类对象对该类的成员函数进行调用
	Int t1(2), t2(4), t3(6), t4(8), t5(10);
	vector<Int> Iv;
	Iv.push_back(t1);
	Iv.push_back(t2);
	Iv.push_back(t3);
	Iv.push_back(t4);
	Iv.push_back(t5);
	for_each(Iv.begin(), Iv.end(), mem_fun_ref(&Int::print1));
	cout << endl;
	system("pause");
	return 0;
}

 


2. container adapters

stack和queue,参见相关源码

3. Iterator adapters

3.1. insert iterators

insert iterators 的实现中,其中的主要观念是,每一个insert iterators 内部都维护有一个容器(必须由用户指定);容器当然有自己的迭代器,于是,当客户端对insert iterators做赋值操作时,就在insert iterators中被转为对该容器的迭代器做插入操作,也就是说,在insert iterators的operator=操作符中调用底层容器的push_front()或push_back()或insert()操作函数。至于其他的迭代器惯常行为如 operator++, operator++(int), operator* 都被关闭功能,更没有提供 operator--(int) 或 operator--或 operator->等功能(因此类型被定义为output_iterator_tag)。换句话说,insert iterators的前进、后退、取值、成员取用等操作都是没有意义的,甚至是不允许的。 参见相关源码;

3.2. reverse iterators

reverse iterator, 就是将迭代器的移动行为逆转。只要是双向序列容器提供了begin(), end() ,它的rbegin(), rend() 就是使用reverse iterator。单向序列容器slist 不可使用reverse iterators。有些容器如stack,queue,priority_queue并不提供begin(), end(),当然也就没有rbegin(), rend()。 当迭代器被逆转,虽然实体位置不变,但逻辑位置必须改变。逆转时,会为了配合迭代器区间的“前闭后开”的特性作相应的调整。 参见相关源码;

3.3. stream iterators

stream iterators,可以将迭代器绑定到一个stream(数据流)对象身上。绑定到istream对象(如std::cin)者,称为istream_iterator,拥有输入能力;绑定到ostream对象(如std::cout)者,称为ostream_iterator,拥有输出能力。 参见相关源码;

4. function adapters

4.1 对返回值进行逻辑否定:

not1, not2 参见相关源码;

4.2 对参数进行绑定:

bind1st,bind2nd 参见相关源码;

4.3 用于函数合成:

compose1, compose2

compose1,对两个可配接函数f(), g(), 产生一个h(), 使 h(x) = f(g(x));

compose2,对三个可配接函数f(), g1(), g2(), 产生一个h(), 使 h(x) = f(g1(x), g2(x));

参见相关源码;

4.4 用于函数指针:

ptr_fun 参见相关源码;

4.5 用于成员函数指针:

mem_fun, mem_fun_ref

这种配接器使我们能够将成员函数当做仿函数来使用,于是成员函数可以搭配各种泛型算法。当容器的元素型式是X& 或X* ,而我们又以虚拟成员函数作为仿函数,便可以藉由泛型算法完成所谓的多态调用。这是泛型与多态之间的一个重要接轨。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值