当前位置: 首页 > news >正文

【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表达式中,最值得谈的当然是它的捕获列表啦。

捕获方式

语法

效果

按值捕获

[x]

复制外部变量 x 的值到 Lambda 对象中

按引用捕获

[&x]

引用外部变量 x,Lambda 内修改会影响原变量

捕获全变量(值)

[=]

所有外部变量按值捕获(存在悬挂引用风险,需谨慎!)

捕获全变量(引用)

[&]

所有外部变量按引用捕获(可能无意中修改外部状态,慎用!)

混合捕获

[x, &y]

按值捕获 x,按引用捕获 y

捕获 this

[this] 或 [=]

隐式捕获类的 this 指针,可访问类成员变量

 

可以看到,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

生成一个 obj 拷贝,绑定到拷贝后的对象(可能导致状态不一致)

std::bind(&MyClass::Method, obj, ...)

按引用传递 std::ref(obj)

绑定到原始对象,但需确保对象生命周期在调用时有效

std::bind(&MyClass::Method, std::ref(obj), ...)

按指针传递 &obj

直接使用指针指向的原始对象

std::bind(&MyClass::Method, &obj, ...)

结语

引用一位大佬的一句话——“Use lambdas if you can, and std::bind if you must.这句话出自 Scott Meyers 的经典著作《Effective Modern C++》,书中明确指出:与 std::bind 相比,lambda 几乎在所有方面更优越。


感谢支持!

相关文章:

  • LeetCode 3396 题解
  • 安装vllm
  • 【mllm】——x64模拟htp的后端无法编译debug
  • MySQL深分页问题
  • 【Code】《代码整洁之道》笔记-Chapter11-系统
  • Cuto壁纸 2.6.9 | 解锁所有高清精选壁纸,无广告干扰
  • 单细胞多组学及空间组学数据分析与应用
  • 《系统分析师-浏览试卷(一)总结》
  • 元生代品牌建设:平台实现工作流(comfyui)创建与技术文档说明
  • CVE-2025-32375 | Windows下复现 BentoML runner 服务器远程命令执行漏洞
  • JavaScript:基本语法
  • 电脑的usb端口电压会大于开发板需要的电压吗
  • 【从零开始学习JVM | 第二篇】HotSpot虚拟机对象探秘
  • ai-warp 开源的Platformatic Stackable 与 AI 服务交互
  • 快速idea本地和推送到远程仓库
  • .net 使用笔记
  • 【DDR 内存学习专栏 1. -- DDR 内存带宽与 CPU 速率】
  • 【Hadoop入门】Hadoop生态之Oozie简介
  • windows sc 创建删除服务
  • Java设计模式之享元模式:从入门到架构级实践
  • 潜力的网站设计制作/营销软文小短文
  • 网站建设需要几个阶段/seo搜索推广费用多少
  • 免费wordpress网站/万网域名注册查询
  • 互诺科技做网站怎么样/全国疫情一览表
  • 济宁网站建设平台/百度app下载安装官方免费版
  • 怎么做电影网站不违法/南京seo外包