本讲我们将介绍函数对象,尤其是匿名函数对象——lambda 表达式。今天的内容说难不难,但可能跟你的日常思维方式有较大的区别,建议你一定要试验一下文中的代码(使用 xeus-cling 的同学要注意:xeus-cling 似乎不太喜欢有 lambda 的代码😓;遇到有问题时,还是只能回到普通的编译执行方式了)。
C++98 的函数对象
函数对象(function object)[1] 自 C++98 开始就已经被标准化了。从概念上来说,函数对象是一个可以被当作函数来用的对象。它有时也会被叫做 functor,但这个术语在范畴论里有着完全不同的含义,还是不用为妙——否则玩函数式编程的人可能会朝着你大皱眉头的。
下面的代码定义了一个简单的加 n 的函数对象类(根据一般的惯例,我们使用了 struct 关键字而不是 class 关键字):
struct adder {
adder(int n) : n_(n) {}
int operator()(int x) const
{
return x + n_;
}
private:
int n_;
};
它看起来相当普通,唯一有点特别的地方就是定义了一个 operator(),这个运算符允许我们像调用函数一样使用小括号的语法。随后,我们可以定义一个实际的函数对象,如 C++11 形式的:
auto add_2 = adder(2);
或 C++98 形式的:
adder add_2(2);
得到的结果 add_2 就可以当作一个函数来用了。你如果写下 add_2(5) 的话,就会得到结果 7。
C++98 里也定义了少数高阶函数:你可以传递一个函数对象过去,结果得到一个新的函数对象。最典型的也许是目前已经从 C++17 标准里移除的 bind1st 和 bind2nd 了(在 <functional> 头文件中提供):
auto add_2 = bind2nd(plus<int>(), 2);
这样产生的 add_2 功能和前面相同,是把参数 2 当作第二个参数绑定到函数对象 plus<int>(它的 operator() 需要两个参数)上的结果。当然,auto 在 C++98 里是没有的,结果要赋给一个变量就有点别扭了,得写成:
binder2nd<plus<int> > add_2(
plus<int>(), 2);
因此,在 C++98 里我们通常会直接使用绑定的结果:
#include <algorithm>
#include <functional>
#include <vector>
using namespace std;
vector v{1, 2, 3, 4, 5};
transform(v.begin(), v.end(),
v.begin(),
bind2nd(plus<int>(), 2));
上面的代码会将容器里的每一项数值都加上 2(transform 函数模板在 <algorithm> 头文件中提供)。可以验证结果:
v
{ 3, 4, 5, 6, 7 }
函数的指针和引用
除非你用一个引用模板参数来捕捉函数类型,传递给一个函数的函数实参会退化成为一个函数指针。不管是函数指针还是函数引用,你也都可以当成函数对象来用。
假设我们有下面的函数定义:
int add_2(int x)
{
return x + 2;
};
如果我们有下面的模板声明:
template <typename T>
auto test1(T fn)
{
return fn(2);
}
template <typename T>
auto test2(T& fn)
{
return fn(2);
}
template <typename T>
auto test3(T* fn)
{
return (*fn)(2);
}
当我们拿 add_2 去调用这三个函数模板时,fn 的类型将分别被推导为 int (*)(int)、int (&)(int) 和 int (*)(int)。不管我们得到的是指针还是引用,我们都可以直接拿它当普通的函数用。当然,在函数指针的情况下,我们