函数对象与lambda表达式
1 什么是函数对象
1.1 函数对象
函数对象是任何可以被当做函数来使用的对象。
class Adder {
Adder(int n) : n_(n) {}
int operator()(int x) const { return x + n_; }
private:
int n_;
};
上述定义了一个普通的类型,唯一特别的地方是在类中定义了一个operator(),这个运算法符允许我们想调用函数一样使用小括号的语法。如:Adder add2(2);此时,得到的add2就有可以当一个函数来使用了,如果我们写下add2(5)的话,就得到结果7。由于运算符重载的作用,这个表达式实际作用相当于add2.operator()(5)。需要注意的地方是operator()通常是const成员函数,即应显式声明调用这个成员函数不会修改对象的状态。
1.2 函数对象与STL算法的结合
函数对象可用于自定义算法行为,如排序、过滤等:
例程:
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
template <typename T>
class LessThan {
public:
LessThan() = default;
bool operator()(T a, T b) {
return a < b;
}
};
int main() {
vector<int> vec{5, 9, 7, 1, 6, 3, 2, 8};
LessThan<int> ls;
sort(vec.begin(), vec.end(), ls);
for (const auto &x : vec) {
cout << x << endl;
}
return 0;
}
这里,我们介绍了函数对象以及函数对象的使用场景,但是,上述例程中,使用函数对象与std::sort实现升序排序的代码略显冗长,有没有其他简洁的方法呢?其实是有的,我们可以改写成如下:
sort(vec.begin(), vec.end(), [](int a, int b) {return a < b;});
这里的写法就是C++的lambda表达式。
2 lambda表达式
2.1 基本语法
lambda表达式的完整语法为:
[捕获列表](参数列表) mutable/异常说明符 -> 返回类型 { 函数体 }
- 捕获列表:决定外部变量的访问方式(值捕获、引用捕获或者混合捕获)
- 参数列表:与普通函数参数类似,可省略(无参数时)
- mutable:允许修改修改按值捕获的变量(默认lambda表达式为
const函数) - 返回类型:可自动推导,但是多分支返回需要显示指定返回值类型
auto sum = [](int a, int b) -> int { return a + b; }; //有参数,显示指定返回值
auto print = [] { std::cout << "Hello"; }; //无参数,返回void
2.2 捕获列表的规则
- 值捕获[x]:复制变量到闭包,外部修改不影响内部(需
mutable才可以修改副本) - 引用捕获[&x]: 直接操作外部变量,修改影响原值
- 隐式捕获:
[=]:按值捕获所有变量(包括this,但是在C++20开始已弃用)[&]:按引用捕获所有变量。
- 混合捕获:如
[=, &x](值捕获全部,x按引用捕获)或[&, x](引用捕获全部,x按值捕获) - 特殊方式捕获:
[this]:在成员函数里捕获当前对象的this指针,以便在lambda表达式里访问当前对象的成员。这里语法是属于值捕获,但实际效果更接近“按引用捕获”。[*this]:在成员函数里捕获当前对象的一个副本,以便在lambda表达式里访问这个对象副本的成员- 用“变量 = 表达式”捕获表达式的值到指定的变量里
- 用“&变量 = 表达式”捕获表达式的引用到指定的变量里。
示例:
int x = 1, y = 2;
auto lambda = [=, &y]() mutable {
x++; //修改副本,不影响外部x
y++; //修改外部y
};
此外,需要注意的一点是,在按引用捕获时你仍可“修改”捕获的变量,因为此时我们修改的不是引用,而是引用指向的对象:也就是说不需要mutable就可以在lambda表达式里修改按引用捕获的对象的源对象。
示例
int x = 1;
auto lambda = [&x]() /*mutable*/ { //无需mutable就可以修改引用的源对象
x++; //修改外部x
};
2.3 lambda的实现原理
编译器会将lambda转换为一个匿名类,重载operator
//lambda: [x](int a) { return a + x; }
class Closure {
private:
int x;
public:
Closure(const int& x) : x(x) {}
int operator()(int a) const { return a + x; }
};
//lambda: [&x](int a) { return a + x; }
class Closure {
private:
int &x;
public:
Closure(int &x) : x(x) {}
int operator()(int a) const { return a + x; }
}
闭包对象在运行时创建,捕获的变量作为类的成员。也就是说,按值捕获会把被捕获的变量的值存在函数对象里,而按引用捕获会把被捕获变量的引用存到函数对象里。
2.4 易错点与注意事项
1.捕获变量的声明周期
- 悬挂引用:若lambda捕获局部变量的引用,而变量已经销毁,将导致未定义行为。
auto create_lambda() {
int x = 10;
return [&x]() { return x; }; //x 已经销毁,调用时奔溃
}
2.返回类型推导的陷阱
- 自动推导失败:若lambda体包含多分支返回类型不一致的语句,需要显示指定返回类型.
auto lambda = [](int x){
if (x > 0) {
return 1.0;
} else {
return 0; //需同一返回类型
}
}
//g++ test3.cpp -- error: inconsistent types ‘double’ and ‘int’ deduced for lambda return type
3.线程安全问题
- 引用捕获与多线程:若多个线程通过引用捕获共享变量,需要同步机制避免数据竞争。
2.5 高级特性
1.初始化捕获: 允许在捕获时初始化变量(支持右值)
auto ptr = std::make_unique<int>(10);
auto lambda = [v = std::move(ptr)] { return *v; };
2.泛型lambda:参数支持auto类型推导
auto add = [](auto a, auto b) { return a + b; };
浙公网安备 33010602011771号