STL之适配器
适配器之概览及分类
适配器(adapters)在STL组件的灵活组合运用功能上,扮演者轴承、转换器的角色。Adapter这个概念,事实上是一种设计模式(design pattern)。《Design Patterns》一书中提到23个最普及的设计模式,其中对adapter样式的定义如下:将一个class的接口转换为另一个class的接口,使原本因接口不兼容而不能合作的class,可以一起运作。
STL所提供的各种适配器中,改变仿函数(functors)接口者,我们称之为function adapter,改变容器(containers)接口者,我们称之为container adapter,改变迭代器(iterators)接口者,我们称之为iterator adapter。
应用于容器,container adapters
STL提供的两个容器queue和stack,其实都只不过是一种适配器。它们修饰deque的接口而成就出另一种容器风貌。这两个container adapters已于stack和queue中做了介绍。
应用于迭代器, iterator adapters
STL提供了许多应用迭代器身上的适配器,包括insert iterators, reverse iterators, iostream iterators。C++ Standard规定它们的接口可以藉由<iterator>获得,SGI STL则将它们实际定义于<stl_iterator.h>。
Insert Iterators
所谓insert iterrators,可以将一般迭代器的赋值(assgin)操作转变为插入(insert)操作。这样的迭代器包括专司尾端插入操作的back_insert_iterator,专司头端插入操作的front_insert_iterator,以及可以从任意位置执行插入操作的insert_iterator。由于这三个iterator adapters的使用接口不是十分直观,给一般用户带来困扰,因此,STL更提供三个相应函数:back_inserter(),front_inserter(),inserter();如下所示:
| 辅助函数(helper function) | 实际产生的对象 |
|---|---|
| back_inserter(Container&x); | back_insert_insertor<Container>(x); |
| front_inserter(Container&x); | front_insert_insertor<Container>(x); |
| inserter(Container&x, Iterator i); | insert_insertor<Container>(x, Container::iterator(x)); |
Reverse Iterators
所谓reverse iterators,可以将一般迭代器的行进方向逆转,是原本应该前进的operator++变成后退操作,使原本应该后退的operator–变成了前进操作。这种错乱的行为不是为了掩人耳目或为了欺敌效果,而是因为这种倒转筋脉的性质运用于“从尾端开始进行”的算法上,有很大的方便性。稍后我们会有一些范例展示。
IOStream Iterators
所谓iostream iterators,可以将迭代器绑定在某个iostream对象身上。绑定到istream对象上(例如std::cin)身上的,称为istream_iterator,拥有输入功能;绑定在ostream对象(例如std::cout)身上的,称为ostream_iterator,拥有输出功能。这种迭代器运用于屏幕输出,非常方便。以它为蓝图,稍加修改,便可适用于任何输出或输入装置上。例如,你可以在透彻了解iostream Iterators的技术后,完成一个绑定到Internet Exploer cache身上的迭代器1,或是完成一个绑定到磁盘目录上的一个迭代器。
请注意,不像稍后即将出场的仿函数配接器(function adapters)总以仿函数作为参数,予人以“拿某个适配器来修饰某个仿函数”的直观感受,这里所介绍的迭代器适配器(iterator adapters)很少以迭代器为直接参数2。所谓对迭代器的修饰,只是一种观念上的改变(复制操作变为插入操作,前进变后退,绑定到特殊装置上等)。我们可以千变万化地写出适合自己所用的任何迭代器。就这一点而言,为了将STL灵活运用于我们的日常编码中,iterator adapters的技术是非常重要的。
下面是一个实例,集上述三种iterator adapters之运用大成:
#include <iterator>
#include <deque>
#include <algorithm>
#include <iostream>
using namespace std;
int main() {
ostream_iterator<int> outite(cout, " ");
int ia[] = {0,1,2,3,4,5};
deque<int> id(ia, ia+6);
copy(id.begin(), id.end(), outite); // 0 1 2 3 4 5
cout << endl;
copy(ia + 1, ia + 2, front_inserter(id));
copy(id.begin(), id.end(), outite); // 1 0 1 2 3 4 5
cout << endl;
copy(ia + 3, ia + 4, back_inserter(id));
copy(id.begin(), id.end(), outite); // 1 0 1 2 3 4 5 3
cout << endl;
auto ite = find(id.begin(), id.end(), 5);
copy(ia + 0, ia + 2, inserter(id, ite));
copy(id.begin(), id.end(), outite); // 1 0 1 2 3 4 0 1 5 3
cout << endl;
copy(id.rbegin(), id.rend(), outite); // 3 5 1 0 4 3 2 1 0 1
cout << endl;
// 此处将标准输入的数据插入inite,使用文件重定向的方式输出
// 实际运行方式为./a.out < input.txt
// input.txt 的内容为 32 22 69
istream_iterator<int> inite(cin), eos;
copy(inite, eos, inserter(id, id.begin()));
copy(id.begin(), id.end(), outite); // 32 22 69 1 0 1 2 3 4 0 1 5 3
cout << endl;
return 0;
}
应用于仿函数, functor adapters
functor adapters(亦称为function adapters)是所有适配器中数量最庞大的一个族群,其适配灵活度也是前二者所不能及,可以适配、适配、再适配。这些适配操作包括bind,negate,compose,以及对一般函数或成员函数的修饰(使其称为一个仿函数)。C++ Standard规定这些适配器的接口可由<functional>获得,SGI STL则将它们实际定义于<stl_function.h>。
function adapters的价值在于,通过它们之间的绑定、组合、修饰能力,几乎可以无限制的创建各种可能得表达式(expression),搭配STL算法一起演出。例如,我们可能希望找出某个序列中所有不小于12的元素个数。虽然,“不小于”就是“大于或等于”,我们因此可以选择STL内建的仿函数greater_equal,但如果希望完全遵循实际语意(在某些更复杂的情况下,这可能是必要的),坚持找出“不小于”12的元素个数,可以这么做:
not1(bind2nd(less<int>(), 12))
这个表达式将less<int>()的第二个参数绑定为12,再加上否定操作,便形成了“不小于12”的语意,整个凑合成为一个表达式。可于任何“可接受表达式为参数”之算法搭配----是的,几乎每个STL算法都有这样的版本。
再举一个例子,假设我们希望对序列中的每一个元素都做特殊运算,这个运算的数学表达式为:
f(g(elem))
其中f和g都是数学函数,那么可以这么写:
compose1(f(x), g(x));
例如我们希望将容器内的每一元素v进行(v+2)3的操作,我们可以另f(x)=x3;g(y)=y+2;并写下这样的表达式:
compose1(bind2nd(multiplies<int>(), 3), bind2nd(plus<int>(), 2));
//第一个参数被拿来当做f(),第二个参数被拿来当做g()
这一长串形成一个表达式,可以拿来和任何接受表达式的算法匹配。不过,务请注意,这个算式会改变参数的值,所以不能和non-mutating算法搭配。例如不能和for_each搭配(调用了后不会影响原迭代器内的值),但可以和transform搭配,将结果输往另一个地点,下面是是实例。
#include <iterator>
#include <functional>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
template <class Func1, class Func2>
struct compose1 : unary_function<typename Func2::argument_type, typename Func1::result_type>{
compose1(Func1 func1, Func2 func2) : func1_(func1), func2_(func2) {}
typename Func1::result_type
operator() (const typename Func1::argument_type & x) {
return func1_(func2_(x));
}
protected:
Func1 func1_;
Func2 func2_;
};
int main() {
ostream_iterator<int> outite(cout, " ");
int ia[] = {2, 21, 12, 7, 19, 23};
vector<int> iv(ia, ia+6);
for_each(iv.begin(), iv.end(), compose1(bind2nd(multiplies<int>(),3),
bind2nd(plus<int>(),2)));
copy(iv.begin(), iv.end(), outite); // 2 21 12 7 19 23
cout << endl;
transform(iv.begin(), iv.end(), outite, compose1(bind2nd(multiplies<int>(),3),
bind2nd(plus<int>(),2))); // 12 69 42 27 63 75
cout << endl;
return 0;
}
**以上示例中,包含了compose1的定义,是因为STL将compose1的定义移除了。**
由于仿函数就是“将function call操作符”的一种class,而任何算法接受一个仿函数时,总是在其演算过程中调用该仿函数的operator(),这使得不具备仿函数之行,却有仿函数之实的“一般函数”和“成员函数”感到为难。如果既存的心血不能纳入复用体系中,完美的规划就崩落一角了。为此,STL又提供了为数众多的适配器。使“一般函数”和“成员函数”得以无缝隙地与其它适配器或算法结合起来。当然,STL所提供的这些适配器不可能在变化芬崎的各种应用场合完全满足我们的所有需求,例如它没有提供我们写出“大于5且小于10”或是“大于8或小于6”这样的样式。不过,对STL源代码有了一番彻底研究后,要打造专用的适配器,不是难事。
请注意,所有期望获得适配能力的组件,本身必须是可适配的。换句话说。一元仿函数必须继承自unary_function,二元仿函数必须继承自binary_function,成员函数必须以mem_fun处理过,一般函数必须以ptr_func处理过。一个未经过ptr_func处理过的一般函数,虽然也可以函数指针的形式传给STL算法使用,却无法拥有任何适配能力。
下图是STL function adapters一览表。实际运用我们采用左边的辅助函数而不自行产生右边的对象,因为辅助函数的接口比较直观,比较好用;有些辅助函数还形成了重载(例如mem_func()和mem_func_ref()),更增加了使用上的便利。
| 辅助函数(helper function) | 实际效果 | 实际产生对象 |
|---|---|---|
| bind1st(const Op&op, const T&x) | op(x, param) | binder1st<Op>(op, arg1_type(x)) |
| bind2nd(const Op&op, const T&x) | op(x, param) | binder2nd<Op>(op, arg2_type(x)) |
| not1(const Pred& pred) | !pred(param) | unary_negate<Pred>(pred) |
| not2(const Pred& pred) | !pred(param1, param2) | unary_negate<Pred>(pred) |
| compse1(const Op1&op1, const Op2 & op2) | op1(op2(param)) | unary_compose<Op1, Op2>(op1,op2) |
| compse2(const Op1&op1, const Op2 & op2), const Op3 &op3 | op1(op2(param),op3(param)) | binary_compose<Op1, Op2, Op3>(op1,op2, op3) |
| ptr_fun(Result(*fp)(Arg)) | fp(param) | pointer_to_unary_function<Arg,Result>(fp) |
| ptr_fun(Result(*fp)(Arg1, Arg2)) | fp(param1, param2) | pointer_to_binary_function<Arg1, Arg2,Result>(fp) |
| mem_fun(S (T::*f)()) | (param->*f)() | mem_func_t<S, T>(f) |
| mem_fun(S (T::*f)() const) | (param->*f)() | const_mem_func_t<S, T>(f) |
| mem_fun_ref(S (T::*f)()) | (param.*f)() | mem_ref_func_t<S, T>(f) |
| mem_fun_ref(S (T::*f)() const) | (param.*f)() | const_mem_ref_func_t<S, T>(f) |
| mem_fun1(S (T::*f)(A)) | (param->*f)(x) | mem_func1_t<S, T, A>(f) |
| mem_fun1(S (T::*f)(A) const) | (param->*f)(x) | const_mem_func1_t<S, T, A>(f) |
| mem_fun1_ref(S (T::*f)(A)) | (param.*f)(x) | mem_ref_func_t<S, T, A>(f) |
| mem_fun1_ref(S (T::*f)(A) const) | (param.*f)(x) | const_mem_ref_func_t<S, T, A>(f) |
1:参见C++ and STL: Take Advantage of STL Algorithms by Implementiong a Custiom Iterator, By Samir Bajaj, MSDN Magazine, 2001/04.这篇文章示范如何写出一个迭代器,协助STL算法遍历Internet Explorer cache。这是一个自定义的迭代器,也可以说是一个适配器,因为它让原本互不认识的STL算法和IE cache的以一起运作。 ↩︎
2:通常它们以容器为直接参数,而每个容器都有自己专属的迭代器,因此这里所谈的适配器事实上是以容器的迭代器为间接参数。当然了,C++语法并无所谓间接参数,稍后看实现代码,即可拨云见日。 ↩︎

1万+

被折叠的 条评论
为什么被折叠?



