00 写在前面
仿函数(functors)和适配器(adapter)就到了整个STL的最后部分。虽然这两部分内容不是很多,可以放在一起说。
但作为STL六大部件中的两大部件,这两者还是有很多设计的精华在里面,值得我们学习。
这也是【STL源码剖析】总结笔记的最后一篇。
01 仿函数(Functors)
仿函数的由来
仿函数,顾名思义,就是类似函数的一种东西。在讨论仿函数之前,我们来看看它是如何产生的。
在前面我们见识了很多算法,算法大都提供一个默认版本,但如果用户想根据不同的应用场景对算法进行变换,也是可以的。比如sort排序算法默认是升序的,我们还可以根据操作的不同变为降序等等。
而这里的”操作“就是改变算法的关键。
首先第一种方法是把这个操作写成一个函数,然后在算法的参数中传入函数指针。这样是完全可以实现对算法的改变的,但是会有一个问题,就是不够灵活,不能随意改变。
举个例子,求数组中大于10的数的个数。
int RecallFunc(int *start, int *end, bool (*pf)(int)) {
int count=0;
for(int *i=start;i!=end+1;i++) {
count = pf(*i) ? count+1 : count;
}
return count;
}
bool IsGreaterThanTen(int num) {
return num>10 ? true : false;
}
int main() {
int a[5] = {10,100,11,5,19};
int result = RecallFunc(a,a+4,IsGreaterThanTen);
return 0;
}
我们可以把大于10写为一个函数,再传入原本的算法中。
但如果我们想实现大于任何数,让IsGreaterThanTen(int num)变为IsGreaterThan(int num1,int num2)传入两个参数,那就不可以实现了。(当然定义为全局变量也可以实现)
这时候仿函数就派上用场了。
再举个例子,比如常见的比较大小用的less
template <class T>
struct less:public binary_function<T,T,bool>{
bool operator()(const T&x,const T&y)const{turn x<y};
};
这样定义之后就可以产生一个仿函数对象了。
可以产生仿函数实体,然后调用实体:
less<int> less_obj;
less_obj(3,5);
或者使用临时对象**(常用)**
less<int>()(3,5);
当然,配合算法才是仿函数的目的,只为算法服务。
sort(vi.begin(),vi.end(),less<int>());
### 仿函数与算法
大家可能也注意到了,仿函数需要重载小括号。
而这样的重载可以使其很好地融合于算法之中。
比如算法accumulate,在第二个版本中注意第三个参数
template <class InputIterator,class T,class BinaryOperation>
T accumulate(InputIterator first,InputIterator last,T init,BinaryOperation binary_op){
for(;first!=last;++first)
init=binary_op(init,*first);
return init;
}
第三个参数是binary_op,当这里传入仿函数时,这个重载小括号的操作就可以直接对binary_op()里的参数进行操作了。如果传入的仿函数是plus,那么这里也可以翻译为plus()(init,*first)
仿函数的继承
那我们是不是也可以设计仿函数呢?当然是可以的。
比如
struct myclass{
bool operator()(int i,int j){
return i<j;
}
}myobj;
这样看上去没有什么问题,调用的时候也可以正常调用。
但仔细和上面的less作比较,就可以发现区别在哪里。
less继承了一个public binary_function<T,T,bool>
这个继承是让仿函数融入STL体系的关键。
仿函数的可配接性
继承关系下,有两个结构体,分别是unary_function和binary_function。
定义也很简单,主要用于表现函数的参数类型和返回值类型。
unary_function对应一元操作,比如取反之类的。
template <class Arg,class Result>
struct unary_function{
typedef Arg argument_type;
typedef Result sult_type;
};
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;
};
如果仿函数继承了unary_function或binary_function,那么后续的适配器(adapter)就可以获取到仿函数的各种类型。
这样也使得在STL中的仿函数有了像积木一样的拼接能力。
02 适配器(Adapter)
适配器是什么
适配器(Adapter),也叫配接器,其实就是对现有的class进行接口的改变。应用在容器上就叫容器适配器(container adapter),用在仿函数上就叫函数适配器(function adapter),用在迭代器上就叫迭代器适配器(iterator adapter)
如果A想要实现B内含的功能,那么会有两种途径:A继承B或者A内包含B。
对于适配器来说是第二种实现方法。
容器适配器
容器适配器我们在deque部分就已经见过了,就是queue和stack的实现。
它们底层都是依靠deque实现的,所以也被称为容器适配器。
link
template <class T,class Sequence = deque<T>>
class stack{
protected:
Sequence c;
public:
bool empty() const{return c.empty()};
...
}
函数适配器
函数适配器是适配器中的重头戏。我们前面说仿函数的继承也是为了和适配器更好地配合。
用第一篇出现的例子来说明函数适配器
cout<<count_if(vi.begin(),vi.end(),bind2nd(less<int>(),40));
这里的bind2nd就是一个典型的函数适配器。
先看代码
template <class Operation>
class binder2nd:public unary_function<typename Operation::first_agument_type,typename Operation::result_type>{
protected:
Operation op;
typename Operation::second_argument_type value;
public:
binder2nd(const Operation&x,const typename Operation::second_argument_type&y)
:op(x),value(y){}
typename Operation::result_type
operator(const typename Operation::first_agument_type &x )const{
return op(x,value);
}
};
这段代码的信息量很大,我们一点一点说。
- 首先明白typename的用法,typename可以直接告诉编译器后面的是类型而不是变量,防止编译失败。
- bind2nd的作用就是给仿函数的第二个参数绑定一个值。
- 注意这里写的是binder2nd,是bind2nd的内层函数。
- binder2nd一开始并没有调用less(),而是用op把它记录了下来。
- Operation::second_argument_type是适配器对仿函数提问的环节,然后仿函数告诉适配器第二参数的类型(对于less()来说就是int)
- 最后对小括号进行重载,才真正调用了less()
- 适配器是用来修饰仿函数的,如果仿函数重载了小括号,那么适配器在修饰完后也需要是同样的效果,所以也需要重载小括号。
对于binder2nd来说,用户需要知道Operation是什么类型才可以使用。上例中Operation其实就是less,但这样使用起来很麻烦,所以STL提供了外层接口的包装。
template <class Operation,class T>
inline binder2nd<Operation>bind2nd(const Operation&op,const T&x){
typedef typename Operation::second_argument_type arg2_type;
return binder2nd<Operation>(op,arg2_type(x));
}
首先询问第二参数的类型,然后自动推导出op的类型。
函数适配器和仿函数一样,如果有被继续修改的可能,就会继承unary_function或binary_function。
迭代器适配器
reverse iterator
迭代器适配器中最为有趣的是逆向迭代器,也就是reverse iterator,以从尾到头的形式来处理元素。
我们常用的迭代器是这样:
sort(vi.begin(),vi.end());
逆向迭代器写为
sort(vi.rbegin(),vi.rend());
我们来看rbegin()和rend()的定义
rbegin(){
return reverse_iterator(end());
}
rend(){
return reverse_iterator(begin());
}
可以看出在实现上也是逆向的,头取尾,尾取头。但在实际重载实现的过程中还是有一点区别的。
先来看一张图:
因为前闭后开的原因,我们常见的begin()是起始位置,对应第一个元素,而end()是最后一个元素的下一个位置。
所以在逆向后,rbegin()指的是end()的前一个元素,rend()指的是begin()的前一个元素。
所以重载时要知道前一个元素是什么(但是指针的位置不变)
reference operator*()const{
Iterator tmp=current;
return *--tmp;
}
#### inserter
inserter也是一个迭代器适配器,会把iterator的赋值操作变为安插操作。
inserter的实现主要依靠重载的巧妙。
假设有两个list
如果执行:
list <int>::iterator it=foo.begin();
copy(bar.begin(),bar.end(),inserter(foo,it));
则会从覆盖变为插入
对于copy来说,实现如下:
template <class ImputIterator,class OutputIterator>
OutputIterator copy(InputIterator first,InputIterator last,OutputIterator result){
while(first!=last){
*result=*first;
++result;
++first;
}
return result;
}
过程就是通过first的移动来拷贝原值得到result。
那这里使用inserter是如何实现其他操作的呢?
其实inserter的实现重载了“=”
template <class Container>
class insert_iterator{
...
insert<Container>&operator=(const typename Container::value_type& value){
iter=container->insert(iter,value);
++iter;
return *this;
}
}
是不是非常巧妙,在copy中,当*result=*first时,这个=会调用insert将值插入,再移动指针保持跟随。
这样也就实现了inserter的功能。
X适配器
所谓X适配器,是指除了经典的几大类适配器外,也会被用到的有一些特殊的适配器。比如ostream_iterator和istream_iterator
ostream_iterator
首先来看ostream,常用的是cout
结构上由out_stream和分隔符组成。
class ostream_iterator:public iterator<output_iterator_tag,void,void,void,void>{
basic_ostream<charT,traits>*out_stream;
const chatT*delim;
...
}
来看一个例子体会它的实现:
std::vector<int>myvector;
for(int i=1;i<10;i++)
myvector.push_back(i*10);
std::ostream_iterator<int>out_it(std::cout,",");
std:copy(myvector.begin(),myvector.end(),out_it);
这里在创建ostream时传入了cout和分隔符“,”,相当于先输出一个数再输出一个逗号。
重点在于copy中第三个参数的调用。
copy的实现我们在前面inserter里也见过
template <class ImputIterator,class OutputIterator>
OutputIterator copy(InputIterator first,InputIterator last,OutputIterator result){
while(first!=last){
*result=*first;
++result;
++first;
}
return result;
}
而对于ostream来说,如何让自己融入到类似copy这样的算法中才是关键。
ostream_iterator非常巧妙地重载了“=”,让一切变得合理。
ostream_iterator<T,charT,traits>&operator=(const T& value){
*out_stream<<value;
if(delim!=0)out_stream<<delim;
return *this;
}
遇到=,首先输出value,如果分隔符存在,那么再输出一个分隔符。
istream_iterator
与ostream相对的就是istream,也就是cin之类的操作。
结构上由instream和value组成。
class istream_iterator:public iterator<input_iterator_tag,T,Distance,const T*,const T&>{
basic_istream<charT,traits>*in_stream;
T value;
...
}
举个例子
double value1,value2;
std::istream_iterator<double>eos;
std::istream_iterator<double>iit(std::cin);
if(iit!=eos)value1=*iit;
++iit;
if(iit!=eos)value2=*iit;
可以看到在创建了iit之后,就可以得到value的值了。移动iit后可以得到下一个value。
而这里的实现是istream_iterator的精华。
其实istream_iterator在创建的时候就已经把值存入value了。
istream_iterator(istream_type& s):in_stream(&s){
++*this;
}
...
istream_iterator<T,charT,traits,Distance>& operator++(){
if(in_stream&&!(in_stream>>value)) in_stream=0;
return *this;
}
const T& operator*() const {
return value;
}
在创建时调用了++,而在后面重载了++操作,使value得到输入值。最后*解引用取得value值。
在创建时立刻读取值的操作是istream_iterator中值得注意的点。
03 总结
以上就是【STL源码剖析】总结笔记的全部内容。
涉及了STL的各个方面,不是很细致但也有一定的参考性,需要为工作准备的小伙伴可以当作复习材料从头过一遍,加深记忆。
感谢阅读。