先说一下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* ,而我们又以虚拟成员函数作为仿函数,便可以藉由泛型算法完成所谓的多态调用。这是泛型与多态之间的一个重要接轨。