STL是C++的标准模板库(standard template library),自然其中定义的都是模板。 相比于类和函数声明的显式接口(explicit interface),类模板和函数模板声明的接口属于隐式接口(implicit interface)。 因为模板参数应当满足的接口是由模板中表达式的合法性决定的,这一点给了模板很大的自由。 而函数对象函数指针具有同样的调用语法,因此STL中这两者常常可以互换。

更多关于隐式接口和显式接口的概念和区别,参见Effective C++: Item 41

先来感受一下C++中的函数对象和函数指针:

template<typename T>
void printer(int a, int b, T func){
    cout<<func(a, b)<<endl;
}

在STL中定义了很多像上面这样的模板,这里的T是一个可调用(实现了括号运算符)的东西。 这使得我们在使用模板时可以指定一个计算策略,它可以是函数对象,也可以是函数指针。

Less<int>便是一个常见的函数对象,常用来配置容器或算法。<functional>中定义了很多这样的函数对象。

函数指针

函数指针通常用来将函数传参或存储。例如:

int sum(int a, int b){
    return a+b;
}
int main(){
    printer(2, 3, sum);
    return 0;
}

上述的printer调用方式,编译器会生成对应的函数实例:

void printer(int a, int b, int (*func)(int, int)){
    cout<<func(a, b)<<endl;
}

这里T的类型是int (*)(int, int)

如果你是python或者javascript程序员的话,上述过程没有什么特别的。 唯一要注意的是func的声明方式,星号要和标识符括起来:(*func)

函数对象

函数对象是重载了括号运算符的类的实例,它也可以这样调用:func(a, b)。例如:

class Sum{
public:
    int operator()(int a, int b){
        return a+b;
    }
};

int main(){
    printer(2, 3, Sum());
    return 0;
}

编译器会生成这样的函数实例:

void printer(int a, int b, Sum s){
    cout<<s(a, b)<<endl;
}

函数对象可以实现更加复杂的有状态的运算,因为对象可以有更多的属性和方法。

std::accumulate

#include <iostream>     // std::cout
#include <functional>   // std::minus
#include <numeric>      // std::accumulate

int myfunction (int x, int y) {return x+2*y;}
struct myclass {
	  int operator()(int x, int y) {return x+3*y;}
} myobject;

int main () {
    int init = 100;
    int numbers[] = {10,20,30};
  
    std::cout << std::accumulate(numbers,numbers+3,init) << std::endl;
    std::cout << std::accumulate (numbers, numbers+3, init, std::minus<int>()) << std::endl;
    std::cout << std::accumulate (numbers, numbers+3, init, myfunction) << std::endl;
    std::cout << std::accumulate (numbers, numbers+3, init, myobject) << std::endl;
  
    return 0;
}

在这里accumulate的第四个参数默认值为std::plus<int>(),是一个函数对象, std::minus<int>()也是一个函数对象,它们都是在functional中定义的。 myfunction是函数指针,而myobject是自定义的函数对象。输出如下:

160
40
220
280

摘自cplusplus.com: http://www.cplusplus.com/reference/numeric/accumulate

比较器:std::sort

需要比较元素大小的STL算法、容器的模板、容器的成员函数,都可以给定一个比较策略。它们的默认值通常是Less<T>()

例如,指定greater函数对象作为比较器,就可以实现反向排序:

sort(v.begin(), v.end(), greater<int>());

std::sort要求随机存取迭代器,list不可用std::sort,可以使用list::sort(Pred pr)std::sort实际上是快排,复杂度为$O(n lgn)$,它是不稳定的。stale_sort则是稳定的归并排序。

比较器:模板参数

关联容器通常使用搜索树来实现,所以插入元素时需要进行比较操作。 我们在使用容器模板时可以指定比较器:

class A{
    int n;
public:
    A(int _n):n(_n){}
    bool operator<(const A& lhs, const A& rhs){
        return lhs.n < rhs.n;
    }
};
multiset<A, less<A>> s;
s.insert(A());

默认情况下关联容器使用less模板来进行元素比较,而less中调用了<, 所以默认情况下插入关联容器的元素需要实现<运算符:

如果未实现相应的比较运算符,insert操作会编译错。因为运算符调用本质上是函数调用。

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2015/07/03/stl-function-objects-and-pointers.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。