函数对象(仿函数)

2015/07/30 C和C++基础

提到C++ STL,首先被人想到的是它的三大组件:Containers, Iterators, Algorithms,即容器,迭代器和算法。容器为用户提供了常用的数据结构,算法大多是独立于容器的常用的基本算法,迭代器是由容器提供的一种接口,算法通过迭代器来操控容器。接下来要介绍的是另外的一种组件,函数对象(Function Object,JJHou译作Functor仿函数)

什么是函数对象

顾名思义,函数对象首先是一个对象,即某个类的实例。其次,函数对象的行为和函数一致,即是说可以像调用函数一样来使用函数对象,如参数传递、返回值等。这种行为是通过重载类的()操作符来实现的。

比如:

class Print
{
public:
    void operator()(int n){
        cout<<n<<endl;
    }
};
int main(int argc, char **argv){
    Print print;
    print(372);
    return 0;
}

其实我们早就开始使用函数对象了,当你写下sort(v.begin(), v.end())时(假定v是vector),其实调用的是**sort(v.begin(), v.end(), less())**,这样sort就会将v从小至大排序。若要逆向排序,你就需要显式地为sort指定一个排序规则,即函数对象greater()。 less和greater是STL中的两个模板类,它们使用类型T的<和>操作符。less的一个典型实现可能是这样的:

template <class T>
class less
{
public:
    bool operator()(const T&l, const T&r) const
    {
        return l < r;
    }
};

STL中的函数对象的分类

根据用途和参数特征,STL中的函数对象通常分为以下几类:Predicates, Arithmetic Function Objects, Binders, Negaters, Member Function Adapters, Pointer to Function Adapters。

Predicates

算术运算函数对象

进行简单的算术运算,这类函数对象用的很少,通常是自己定义。

为什么要使用仿函数

1、有些功能的的代码,会在不同的成员函数中用到,想复用这些代码。我们会采用以下方法:

1)公共的函数,可以,这是一个解决方法,不过函数用到的一些变量,就可能成为公共的全局变量,再说为了复用这么一片代码,就要单立出一个函数,也不是很好维护。

比如:C语言使用函数指针和回调函数来实现仿函数,例如一个用来排序的函数可以这样使用仿函数。

#include <stdio.h>  
#include <stdlib.h>    
int sort_function( const void *a, const void *b)  {     
    return *(int*)a-*(int*)b;  
}  
  
int main(){ 
   int list[5]={54,21,11,67,22};  
   qsort((void *)list,5,sizeof(list[0]),sort_function);  //起始地址,个数,元素大小,回调函数   
   int x;  
   for(x=0;x<5;x++)  
		printf("%i\n", list[x]);         
   return 0;  
}  

2)仿函数,写一个简单类,除了那些维护一个类的成员函数外,就只是实现一个operator(),在类实例化时,就将要用的,非参数的元素传入类中。

#include <iostream>  
#include <algorithm>  
using namespace std;  
template<typename T>  
class display{  
public:  
    void operator()(const T &x){  
        cout<<x<<" ";   
    }   
};   
  
int main(){  
    int ia[]={1,2,3,4,5};  
    for_each(ia,ia+5,display<int>());
    return 0;   
}   
2、迭代和计算逻辑分离

使用仿函数可以使迭代和计算分离开来。因而你的functor可以应用于不同场合,在STL的算法中就大量使用了functor,下面是STL中for_each中使用functor的示例:

struct sum{
    sum(int * t):total(t){};
    int * total;
    void operator()(int element){
       *total+=element;
    }
};
int main(){
    int total = 0;
    sum s(&total);
    int arr[] = {0, 1, 2, 3, 4, 5};
    std::for_each(arr, arr+6, s);
    cout << total << endl;               //prints total = 15;
}
3、仿函数比回调函数更加优秀

(1)仿函数可以不带痕迹地传送上下文参数,而回调函数技术通常要使用一个额外的void*参数传送,这也是多数人认为回调函数技术丑陋的原因。

(2)仿函数比回调函数有更好的性能

这里分两种情况比较,比如函数func调用回调函数:

如果func是inline函数并且比较简单,func函数最后会被直接展开,那么其中对回调函数的呼叫也成为一普通的函数呼叫(而不是通过函数指针的间接呼叫),并且如果这个回调函数很简单,那么也可能同时被展开。在这种情形下,回调函数与仿函数性能相同。

如果func是非inline函数的话,或者比较复杂而无法直接展开。此时回调函数作为一个函数指针传入,它的程序代码是无法直接展开的。而仿函数则不同,虽然func本身复杂不能展开,但是func函数中对仿函数的呼叫是编译器在编译期间就可以确定并进行inline展开的。因此在这种情形下,仿函数比回调函数有着更好的性能。并且这种性能优势有时是一种无可比拟的优势,比如对于std::sort就是如此,因为元素比较的次数非常巨大,是否可以进行inline展开导致了一种雪崩效应。

std::transform(in.begin(), in.end(), out.begin(), add_x(1));

编译器可以准确知道std::transform需要调用哪个函数(add_x::operator)。这意味着它可以内联这个函数调用。而如果使用函数指针,编译器不能直接确定指针指向的函数,而这必须在程序运行时才能得到并调用。

一个例子就是比较std::sort 和qsort ,STL的版本一般要快5-10倍。

Search

    Post Directory