0
点赞
收藏
分享

微信扫一扫

【STL源码剖析】总结笔记(12):仿函数(Functors)与适配器(Adapter)

耳一文 2022-02-13 阅读 50
c++stl

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);
 }
};

这段代码的信息量很大,我们一点一点说。

  1. 首先明白typename的用法,typename可以直接告诉编译器后面的是类型而不是变量,防止编译失败。
  2. bind2nd的作用就是给仿函数的第二个参数绑定一个值。
  3. 注意这里写的是binder2nd,是bind2nd的内层函数。
  4. binder2nd一开始并没有调用less(),而是用op把它记录了下来。
  5. Operation::second_argument_type是适配器对仿函数提问的环节,然后仿函数告诉适配器第二参数的类型(对于less()来说就是int)
  6. 最后对小括号进行重载,才真正调用了less()
  7. 适配器是用来修饰仿函数的,如果仿函数重载了小括号,那么适配器在修饰完后也需要是同样的效果,所以也需要重载小括号。

对于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());
}

可以看出在实现上也是逆向的,头取尾,尾取头。但在实际重载实现的过程中还是有一点区别的。

先来看一张图:

image-20211126164108818

因为前闭后开的原因,我们常见的begin()是起始位置,对应第一个元素,而end()是最后一个元素的下一个位置。

所以在逆向后,rbegin()指的是end()的前一个元素,rend()指的是begin()的前一个元素。

所以重载时要知道前一个元素是什么(但是指针的位置不变

reference operator*()const{
    Iterator tmp=current;
    return *--tmp;
}

#### inserter

inserter也是一个迭代器适配器,会把iterator的赋值操作变为安插操作。

inserter的实现主要依靠重载的巧妙。

假设有两个list

image-20211126173736337

如果执行:

list <int>::iterator it=foo.begin();
copy(bar.begin(),bar.end(),inserter(foo,it));

则会从覆盖变为插入

image-20211126173945013

对于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的各个方面,不是很细致但也有一定的参考性,需要为工作准备的小伙伴可以当作复习材料从头过一遍,加深记忆。

感谢阅读。

举报

相关推荐

0 条评论