STL——配接器(adapters)
一、配接器
《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对象身上。
1.3 应用于仿函数——functor adapters
functor adapters,或叫做function adapters,是所有配接器中数量最庞大的一个族群,其配接灵活度也是前二者所不能及,可以配接,配接,再配接。这些配接操作包括绑定(bind),否定(negate),组合(compose),以及对一般函数或成员函数的修饰(使其成为一个仿函数)。C++ Standard 规定这些配接器的接口可由<functional>获得,SGI STL 则将它们实际定义于<stl_function.h>。
function adapters的价值在于,通过它们之间的绑定、组合、修饰能力,几乎可以无限制地创造出各种可能的表达式,搭配STL算法一起演出。例如,我们可能希望找出某个序列中所有不小于12 的元素个数,那么可以这么做:
not1(bind2nd(less<int>(), 12)); //(1) less<int>(), 创建一个匿名的临时对象(同时也是仿函数)
又如:我们希望将容器内的每一个元素v进行(v+2)*3 的操作,我们可以令f(x) = x * 3, g(y) = y + 2;并 写下这样的式子:
compose1(bind2nd(multiplies<int>(), 3), bind2nd(plus<int>(), 2)); // (2)第一个参数被拿来当做f(), 第二个参数被拿来当做g(). // f(g(elem)); 可以写成:compose1(f(x), g(y));
上面(2)表达式,可以拿来和任何接受表达式的算法搭配。不过,务请注意,这个算式会改变参数的值,所以不能和non-mutating算法搭配。例如不能和for_each搭配,但可以和transform搭配,将结果输往另一地点。
请注意,所有期望获得配接能力的组件,本身都必须是可配接的(adaptable)。换句话说,一元仿函数必须继承自unary_function,二元仿函数必须继承自binary_function,成员函数必须以mem_fun处理过,一般函数必须以ptr_fun处理过。一个未经ptr_fun处理过的一般函数,虽然也可以函数指针(pointer to function)的形式传给STL算法使用,却无法拥有任何配接能力。
图8-2是STL function adapters一览表
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
一般而言,对于C++ template 语法有了某种程度的了解之后,我们很能够理解或想象,容器是以class template 完成的,算法以function template 完成,仿函数是一种将 operator() 重载的 class template,迭代器则是一种将 operator++ 和 operator* 等指针惯常行为重载的class template。然而配接器呢?应用于容器身上和迭代器身上的配接器,已于上面介绍过,都是一种 class template。可应用于仿函数身上的配接器呢?如果能够“事先”对一个函数完成参数的绑定、执行结果的否定、甚至多方函数的组合?请注意我用“事先”一词。我的意思是,最后修饰结果(视为一个表达式,expression)将被传给STL算法使用,STL算法才是真正使用这个表达式的主格。而我们都知道,只有在真正使用(调用)某个函数(或仿函数)时,才有可能对参数和执行结果做任何干涉。
那么仿函数怎么实现对参数和返回值的干涉呢?关键在于,就像本章先前所揭示,container adapters 内藏了一个container memeber 一样,或是像reverse iterator(adapters)内藏了一个pointer to stream一样,或是像insert iterator(adapters)内藏了一个pointer to container(并因而得以取其iterator)一样,每一个function adapters 也内藏了一个member object,其型别等同于它所要配接的对象(那个对象当然是一个“可配接的仿函数”,adaptable functor),下图8-6是一份整理。当function adapter 有了完全属于自己的一份修饰对象(的副本)在手的时候,它就成了该修饰对象(的副本)的主人,也就有资格调用该修饰对象(一个仿函数),并在参数和返回值上面动手脚了。
binder2nd 中,构造函数中的x参数是仿函数;operator() 运算子中的参数x是 当运算子operator() 被调用时 由调用者传递进来的第一个参数。
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* ,而我们又以虚拟成员函数作为仿函数,便可以藉由泛型算法完成所谓的多态调用。这是泛型与多态之间的一个重要接轨。
#include <iostream> #include <vector> #include <algorithm> #include <functional> using namespace std; class Shape { public: virtual void display() = 0; }; class Rect : public Shape { public: virtual void display() { cout << "Rect"; }; }; class Circle : public Shape { public: virtual void display() { cout << "Circle"; }; }; class Square : public Rect { public: virtual void display() { cout << "Square"; }; }; int main() {
// 容器只支持对象语义,不支持引用语义 vector<Shape*> V; V.push_back(new Rect); V.push_back(new Circle); V.push_back(new Square); V.push_back(new Circle); V.push_back(new Rect); // 多态调用(polymorphically) for(int i = 0; i < V.size(); ++i) (V[i])->display(); cout << endl; // Rect Circle Square Circle Rect // for_each(V.begin(), V.end(), mem_fun(&Shape::display)); cout << endl; // Rect Circle Square Circle Rect }
(1)请注意,就语法而言,你不能写:
for_each(V.begin(), V.end(), &Shape::display);
也不能写:
for_each(V.begin(), V.end(), Shape::display);
一定要以配接器mem_fun 修饰 member function,才能被算法for_each接受。
(2)另一个必须注意的是,虽然多态可以对pointer或reference起作用,但很可惜的是,STL容器只支持“实值语意”,不支持“引用语意”,因此下面这样无法通过编译: vector<Shape&> V; 参见STL概论 提示17-9;