C++函数对象与lambda表达式

函数对象,一个类如果将()运算符重载,那么这个类就称为函数对象类,使用形式看起来像函数调用,实际执行了函数调用,因此坐函数对象。lambda表达式也称匿名函数,是无需定义标识符(函数名)的函数,如果函数只使用一次或者有限的次数,使用lambda表达式会比较方便。

1. 函数对象的如何实现?

C++中将对象当做函数使用,是函数式编程的思想。这里不作深入讨论,只讨论如何使用;举个简单例子:
【1】无入参的函数对象:

1
2
3
4
5
6
7
8
class ObjType
{
public:
void operator() () // 无参数类型的()重载
{
cout<<"Hello C++!"<<endl;
}
};

【2】有参数的函数对象:

1
2
3
4
5
6
7
8
class ClcAverage
{
public:
double operator()(int a1, int a2, int a3) // 三个入参
{
return (double)(a1 + a2 + a3) / 3;
}
};

【3】函数对像的使用
STL有个accumulate函数,在定义函数模板的时候,使用的是一个函数对象;它的功能是对 [first, last) 中的每个迭代器 对象 执行 val = opt(val, *I),返回最终的 val。Dev C++,定义这样的:

1
2
3
4
5
6
7
8
template <class InIt, class T, class Pred>
T accumulate(InIt first, InIt last, T val, Pred opt)
{
for (; first != last; ++first) {
init = opt(init, *first);
}
return init;
}

模板被实例化之后,opt(init, *first)需要定义,opt只能是函数指针或者函数对象。在使用时,实参只能传递函数名,函数指针或者函数对象。这里以函数对象为例举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <vector>
#include <numeric>
template<class T>
class SumPow
{
public:
SumPow(int pow) :m_pow(pow) { }
const T operator() (const T& total, const T& value)
{ //计算 value的pow次方和
T v = value;
for (int i = 0; i < m_pow - 1; ++i) { v = v * value; }
return total + v;
}
private:
int m_pow;
};
int SumSquare(int total, int value)
{
return total + value * value;
}
int main()
{
int a1[] = { 1, 2, 3 };
vector<int> a2 = { 1, 2, 3 };
// 实例化为函数指针
cout << "SumSquares: " << accumulate(a2.begin(), a2.end(), 0, SumSquare) << endl;
cout << "SumSquares " << accumulate(begin(a1), end(a1), 0, SumSquare) << endl;
// 实例化为函数对象
cout << "SumPow: " << accumulate(a2.begin(), a2.end(), 0, SumPow<int>(2)) << endl;
cout << "SumPow: " << accumulate(begin(a1), end(a1), 0, SumPow<int>(2)) << endl;
return 0;
}

示例中,参数是SumSquare编译器实例化成函数的类似于如下的定义:形参opt是函数指针,而opt(init, *first)就调用了指针opt指向的函数SumSquare

1
2
3
4
5
6
int accumulate(vector <int>::iterator first, vector <int>::iterator last, int init, int(*op)(int, int))
{
for (; first != last; ++first)
init = op(init, *first);
return init;
}

参数SumPow<int>(2)编译器实例化成了函数对象,有等效于如下定义的形式,形参是函数对象,相当于调用opt.operator()(init, *first);,也就是SumPow类的operator函数;

1
2
3
4
5
6
int accumulate(vector<int>::iterator first, vector<int>::iterator last, int init, SumPowers<int> op)
{
for (; first != last; ++first)
init = op(init, *first);
return init;
}

2.lambda表达式

在使用STL的时候,往往需要用很多函数对象或者函数指针。这些对象或者函数指针大部分是一次性,编写在需要的时候编写这样的代码有点浪费。对于只使用一次的函数对象或者函数,lambda表达式是一个有效的解决办法。Lambda表达式也是的闭包的同义词,但是两者也是有区别的,闭包是任何能够访问不在局部和参数列表里定义里面的自由变量的函数实例。C++中的lambda在C++11中才支持。Lambda 表达式的定义形式如下:

1
[外部变量访问方式说明符](参数表)->返回值类型{语句块}

外部变量访问方式说明符=&,表示{}中用到的按值捕获还是按引用捕获, 按值捕获表示定义在{}外面的变量,在{}中不允许被改变,按引用捕获表示允许修改。当然,在{}中也可以不使用定义在外面的变量。->返回值类型可以省略。如下是一个lambda表达式
如果上边不够直观的话,看下边这个图,就更一幕了然。其中,1是捕获说明符;2是捕获参数列表;3是可选的mutable声明;4是可选的异常声明;5是返回类型;6是lambda操作域;
image

捕获列表 capture clause

捕获 作用 捕获 作用
[] 不捕获任何变量 [] 不捕获任何变量
[&] 引用捕获所有变量 [=] 以值捕获所有变量
[&x] 引用只捕获x [x] 以值只捕获x
[&, x] 引用捕获所有变量,x是例外 [=, &x] 值捕获所有变量,其中x是例外
[this] 以引用捕获当前对象 [*this] 以传值方式捕获当前对象

捕获列表 capture clause
举个例子,如果lambda主体total通过引用捕获变量,factor通过值捕获变量,则以下捕获子句是等效的:

1
2
3
4
5
6
[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]

如果捕获子句默认& 按引用捕获,那么identifier在capture捕捉列表中不能再有以引用捕获的形式也就是& identifier。同样,如果capture默认按值捕获capture-default =,则capture该捕获列表不能有按值捕获的形式= identifier。而且,标识符不能在捕获列表中出现多次。需要注意的是默认在捕获列表没有明确捕获方式的时候[],根据捕获参数列表传参形式决定是值捕获还是引用捕获, 形如[](int &x){x=x*2;}

1
2
3
4
5
6
7
8
9
struct S { void f(int i);};
void S::f(int i) {
[&, i]{}; // OK
[&, &i]{}; // ERROR: 按引用捕获,在声明i按引用捕获多余
[=, this]{}; // ERROR: this when = is the default
[=, *this]{ }; // OK: captures this by value.
[i, i]{}; // ERROR: 重复声明捕获
[=]{++i;}; // ERROR: 值捕获,不能修改对象
}

说了这么多,lambda具体怎么用呢?看一看,下边的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
int a[4] = { 11, 1, 37, 9 };
/* 按值捕获 捕获值不可修改*/
sort(a, a + 4, [=](int x, int y) -> bool {return x < y; }); /* 升序排序 */
for_each(a, a + 4, [=](int x) { cout << x << " "; }); /* 依次遍历输出 */

/* 按引用捕获 捕获值不可修改*/
for_each(a, a + 4, [&](int& x) { x *= 2; }); /* 元素平方 */

int x = 1, y=2, z=3;
auto lambdaFun = [=,&y,&z](int n) {
cout <<x << endl;
y++; z++;
return n*n;
};
cout << lambdaFun(15) << endl;
cout << y << "," << z << endl;
}

最后,说一下lambda的使用的注意事项1. 引用捕获可修改外部变量,但值捕获不能。(可变允许修改副本,但不允许修改原件);2. 引用捕获反映了外部变量的更新,但值捕获却没有;3. 引用捕获引入了生命周期依赖,但是值捕获没有生命周期依赖性。当lambda异步运行时,这尤其重要。如果通过异步lambda中的引用捕获本地,则该本地很可能会在lambda运行时消失,从而在运行时导致访问冲突

参考文献:
[1]. https://docs.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp?view=vs-2019

-------------本文结束感谢您的阅读-------------
坚持创作,坚持分享!