【C++取经之路】lambda和bind
目录
引言
lambda语法
lambda捕获列表解析
1)值捕获
2)引用捕获
3)隐式捕获
lambda的工作原理
lambda进阶用法
泛型lambda
立即调用
lambda 与 function
bind语法
bind的调用逻辑
bind核心用途
绑定参数
调整参数顺序
bind的陷阱
bind绑定成员函数
结语
引言
Lambda 是现代 C++(C++11 及之后)引入的核心特性,它使得定义匿名函数(即未命名的代码块)更加便捷,尤其是在需要临时传递函数逻辑时,在写算法题时经常用到,就比如sort默认的排序逻辑(升序)不是我们想要的,我们想要的是降序排列,那么给sort传递一个lambda来排序就很方便。
bind是C++11 标准库中的一个关键工具,可以把它看做是一个函数适配器,它接受一个可调用对象,然后生成一个新的可调用对象来适配原对象的参数列表。
光说概念很抽象,下面会举例子来说明。
lambda语法
[ 捕获列表 ] ( 参数列表 ) -> 返回类型 { 函数体 }
可以看到,lambda表达式主要由5个部分组成。除了捕获列表和函数体,其他的都可以忽略。下面是一个简单的lambda表达式,主要是用来熟悉它的语法。
auto lambda = [](int a, int b) {return a + b; };
cout << lambda(1, 2);//输出3
上述的lambda表达式中,捕获列表为空,参数列表有两个,调用时传入,和普通函数调用传参一样,并没什么特别的,返回类型省略了,因为这种情况下它可以自己推导返回类型。具体什么情况下不能省略这里就不列举啦,当省略后编不过在把返回类型加上即可。总之,规范使用可以避免很多不必要的麻烦。
lambda捕获列表解析
lambda表达式中,最值得谈的当然是它的捕获列表啦。
捕获方式 | 语法 | 效果 |
---|---|---|
按值捕获 |
| 复制外部变量 |
按引用捕获 |
| 引用外部变量 |
捕获全变量(值) |
| 所有外部变量按值捕获(存在悬挂引用风险,需谨慎!) |
捕获全变量(引用) |
| 所有外部变量按引用捕获(可能无意中修改外部状态,慎用!) |
混合捕获 |
| 按值捕获 |
捕获 |
| 隐式捕获类的 |
可以看到,lambda的捕获方式可谓是花里胡哨,下面谈谈不同捕获方式的区别。
1)值捕获
int main()
{
int x = 10;
auto lambda = [x](int y) {return x + y; };
cout << lambda(20);//输出30
return 0;
}
采用值捕获,就是要对捕获的变量进行拷贝,和函数传递参数是采用值传递一样。 只有捕获了该变量才能在lambda的函数体内使用。比如下面的写法就是错误的。
int main()
{
int x = 10, t = 30;
auto lambda = [x]() {return x + t; };//error
return 0;
}
采用值捕获时,还有一个细节,就是lambda函数体内部不能修改通过值捕获进来的变量。 以下是个错误示范。
int main()
{
int x = 10;
auto lambda = [x]() {return x++; };//error
return 0;
}
如果非要修改,也是有办法的。介绍一个关键字——mutable。
int main()
{
int x = 10;
auto lambda = [x]()mutable {return x++; };//正确
return 0;
}
在参数列表后加上mutable关键字即可。
2)引用捕获
int main()
{
int x = 10;
auto lambda = [&x](int y) {
x += 10;
return x + y;
};
lambda(20);
cout << x << endl;//输出20
return 0;
}
引用捕获和函数采用引用方式传递参数是一个道理,不会进行拷贝,lambda函数体内如果对引用捕获的变量进行修改,外部的也就跟着被修改了,本质上就是修改同一个地址的值。
在采用引用捕获时需要注意的一点是:一定要注意所引用对象的生命周期,避免出现野指针。这一点和函数调用结束后返回一个局部变量的引用是一样的道理,在函数调用结束后,局部变量就已经被销毁了,这时引用的地址早就被释放了,再去访问程序就该崩了。
3)隐式捕获
除了我们在捕获列表中显示的指出要捕获哪些变量外,也可以让编译器根据lambda函数体中的代码来推断我们需要捕获哪些变量。即编译器自己推导捕获列表。
int main()
{
int x = 10;
auto lambda = [=](int y) {return x + y; };//采用值捕获的隐式捕获
auto lambda = [&](int y) {return x + y; };//采用引用捕获的隐式捕获
return 0;
}
当我们混用显示捕获和隐式捕获时,必须把=或&放在第一个,比如这样 [=,&x],还有一点就是明明已经采用隐式的值捕获了,又在捕获列表中采用值捕获其他变量,这是不允许的,也大可不必。比如这么写就是不对的[=,x]。总之,还是那句话——规范使用可以避免很多不必要的麻烦。
lambda的工作原理
Lambda 在编译时会生成一个匿名类(称为闭包类),其中捕获的变量就会以成员变量的形式存储,然后该闭包类中还重载了调用运算符,也就是重载了()。下面举个例子。
// Lambda: [x](){ return x * 2; }
class __Lambda_123 { // 编译器生成唯一类名
private:
int x; // 捕获的变量
public:
__Lambda_123(int x_) : x(x_) {}
int operator()() const { return x * 2; }
};
lambda表达式本身就是一个闭包类的实例,也就是闭包类的一个对象。在调用lambda()时,实际上调用的是该闭包类内重载的operator()()函数。
lambda进阶用法
泛型lambda
以auto作为参数类型。
int main()
{
auto print = [](const auto& arg) {cout << arg << endl; };
print(20);
print("hello world!");
return 0;
}
立即调用
定义后立即执行。
int result = [](int a, int b) { return a + b; }(3, 4); // result = 7
lambda 与 function
function是一个函数包装器,它可以将不同形式的可调用对象(如 Lambda、函数指针等)封装到同一类型中。
function<int(int, int)> func = [](int a, int b) {return a + b; };
关于lambda就说到这啦,下面谈谈bind。
bind语法
#include <functional> // 必须包含的头文件
auto bound_fn = std::bind(
FuncPointer, // 原始函数指针/可调用对象
Arg1, Arg2, ..., // 绑定的参数(可包含占位符)
ArgN
);
下面提供个简单代码来熟悉bind的语法。
#include <iostream>
#include <functional>
using namespace std;
int Sum(int a, int b){return a + b;}
int main()
{
auto boundSum = bind(Sum, 10, placeholders::_1);
cout << boundSum(20);//输出30
return 0;
}
bind的调用逻辑
auto boundSum = bind(Sum, 10, placeholders::_1);
bind预先把10绑定到了Sum的第一个参数上,Sum(10, placeholders::_1),placeholders::_1是调用的时候传进来的参数。bind还会把参数拷贝给新的调用对象boundSum,所以如果bind中的有的参数不允许拷贝(输入输出流),那么只能用引用,调用库函数ref,ref(ostream)传递。
bind核心用途
绑定参数
void printSum(int a, int b) {
std::cout << a + b << "\n";
}
// 绑定a=10,剩下参数由调用时提供
auto boundPrint = std::bind(printSum, 10, std::placeholders::_1);
boundPrint(20); // 输出 30(等价于 printSum(10,20))
这段代码就是绑定了printSum的第一个参数,第二个参数调用时在传进来。
调整参数顺序
//f是一个可调用对象,它有5个参数
auto g = bind(f, a, b, placeholders::_2, c, placeholders::_1);
f的第一个、第二个和第四个参数被绑定到了a、b、c上。调用g时,假设这样调用g(X, Y),那么会调用 f (a,b,Y,c,X)。这样子参数顺序就变了。所以占位符的作用顾名思义,就是占着那个坑,调用传参的时候在用参数替代掉占位符。 这里不仅体现了bind可以改变参数顺序的作用,还体现出了——可以把bind看做是一个函数适配器,它接受一个可调用对象,然后生成一个新的可调用对象来适配原对象的参数列表(引言中的抽象概念)。f 原本是需要5个参数的,bind之后,生成的新的可调用对象g就只需要传递两个参数。
bind的陷阱
#include <iostream>
#include <functional>
using namespace std;
class MyClass
{
public:
int Method(int y)
{
_x += 10;
return _x + y;
}
private:
int _x = 10;
};
int main()
{
MyClass *obj = new MyClass();
auto boundMethod = bind(&MyClass::Method, obj, 10);
delete obj;//!!!
boundMethod();//未定义行为
return 0;
}
通过上面的代码,想说明的一点是:一定要注意对象的生命周期。
bind绑定成员函数
比起绑定普通的函数,bind在绑定成员函数时有更多的细节,稍不注意就写错了,得不到期望的结果。
成员函数和普通函数之间,成员函数多了一个this指针,它不能独立于对象存在,必须指明它是哪个对象的成员函数。
MyClass obj;
1) auto boundMethod = bind(MyClass::Method, obj, 10);
2) auto boundMethod = bind(&MyClass::Method, obj, 10);
3) auto boundMethod = bind(&MyClass::Method, &obj, 10);4) auto boundMethod = bind(&MyClass::Method, ref(obj), 10);
下面着重讲讲上面这四种写法的区别。
1)第一种写法,是错误的。 但是这么写 bind(&MyClass::Method, obj, 10); 又是对的。原因在于bind的第一个参数是函数指针或者可调用对象,但是单纯只有类名+函数名MyClass::Method并不是函数地址。因为C++语法规定,成员函数指针必须通过&类名::成员函数名的方式获取,这和普通函数不一样。比如普通函数func,在使用func时会被隐式转换为函数指针,但是成员函数不会。所以第一种写法是错误的,&符号必须得加上。
2)第二种写法和第三种写法的区别在于有没有对obj取地址,我们知道,bind是会对参数进行拷贝的,如果采用第二种写法,那么在调用boundMethod的时候,调用的是拷贝后的obj对象里的Method,所做的修改不会影响原对象obj。并不是说这种写法是不对的,但是可能这不是我们想要的结果。
3)第三种写法就是对obj取地址,这样bind内部直接使用的是原来的obj对象,Method方法里做的修改在原obj中可以看到。
4)第四种写法就是对obj的引用,bind内部直接绑定到原始对象obj,Method方法里做的修改在原obj中也可以被看到。
传递方式 | 含义 | 示例 |
---|---|---|
按值传递对象 | 生成一个 obj 的拷贝,绑定到拷贝后的对象(可能导致状态不一致) |
|
按引用传递 | 绑定到原始对象,但需确保对象生命周期在调用时有效 |
|
按指针传递 | 直接使用指针指向的原始对象 |
|
结语
引用一位大佬的一句话——“Use lambdas if you can, and std::bind
if you must.”这句话出自 Scott Meyers 的经典著作《Effective Modern C++》,书中明确指出:与 std::bind
相比,lambda
几乎在所有方面更优越。
感谢支持!