目录

1.1 lambda表达式语法

1.1.1 什么是lambda表达式

1.1.2 lambda表达式的格式

1.2 捕捉列表

1.2.1 捕捉列表的作用

1.2.2 捕捉规则

1.2.3 捕捉方式

1.3 lambda的应用

1.4 lambda的原理

1.5 lambda与函数指针

1.5.1 普通函数与函数指针

1.5.2 lambda不是函数指针?

1.5.3 为什么无捕获 lambda 可以赋值给函数指针?

1.5.4 有捕获 lambda 为什么不行?

1.5.5 总结:


1.1 lambda表达式语法

1.1.1 什么是lambda表达式

lambda 表达式本质上会生成一个匿名的闭包对象(Closure Object),与普通函数不同的是,它既可以像函数一样被调用,又能够捕获并保存其所在作用域中的变量。Lambda 表达式的类型是一个由编译器生成的、唯一的、匿名的闭包类型(Closure Type),由于该类型名无法直接书写,因此在实际开发中通常使用 auto、模板参数或 std::function 来接收和使用 lambda 对象。

关于闭包对象和闭包类型的具体实现原理,我们将在后续 lambda 原理章节中详细介绍。

1.1.2 lambda表达式的格式

[capture-list] (parameters)-> return type { function body }

[ capture-list]:捕捉列表,该列表位于lambda函数的开始位置,捕捉列表为空也不能省略,因为编译器需要根据捕捉列表来判断接下来的代码是否为lambda函数,具体细节将在后续捕捉列表章节里详细讲解。

(parameters):参数列表,与普通函数的参数列表功能一样,用来接受传递的参数,如果不需要传递参数,则该参数列表可以省略不写。

-> return_type:返回值类型,与普通函数返回值功能一样,但是对于没有返回值的lambda函数,此部分可以省略不写,对于返回值类型明确的函数,此部分也可以省略不写,由编译器根据返回值的类型进行推导返回类型。

{ function body }:函数体,与普通函数的函数体一样,但在该函数体内,可以使用捕捉列表中捕获的变量,函数体为空也不能省略。

示例:

#include <iostream>
using namespace std;

int main()
{
	// 一个完整的lambda表达式
	auto add1 = [](int a, int b)->int
	{
		return a + b;
	}; // 这个分号一定要加,它不是lambda语法所需要的,它是赋值这条语句所需要的

	// 跟函数的使用方法一摸一样
	cout << add1(3, 5) << endl; 

	// 1.捕捉列表为空不能省略
	// 2.参数为空可以省略
	// 3.返回值可以省略
	// 4.函数体不可以省略

	auto print = [] 
	{
		cout << "hello world" << endl;
	};

	print();

	int a = 3, b = 5;
	auto my_swap = [](int& x, int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	}; 

	my_swap(a, b);

	cout << a << " " << b << endl;

	return 0;
}

1.2 捕捉列表

1.2.1 捕捉列表的作用

在lambda表达式中变量的使用规则跟普通函数一样,如lambda函数体和参数中的变量,全局变量等,但lambda通过捕捉列表可以使用lambda对象所处域中的变量。

1.2.2 捕捉规则

普通捕获列表(如[x], [&x])只能捕获具有名字的变量,通常是左值。对于临时对象或移动语义场景,需要使用 C++14 引入的初始化捕获(Init Capture),例如[x = 10] 或 [p = std::move(ptr)]。

// C++11
// 合法
int x = 10;
auto f = [x]()
{
    return x;
};

// 不合法
auto f = [10]()
{
    return 10;
};

// C++14 这里的捕捉不是普通捕捉,而是初始化捕捉,后续lambda底层原理会讲解原因
auto f = [x = 10]()
{
    return x;
};
auto ptr = std::make_unique<int>(100);

auto f = [p = std::move(ptr)]()
{
    return *p;
};

lambda 表达式如果在函数局部域中,它可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda 表达式中可以直接使用。这也意味着lambda 表达式如果定义在全局位置,捕捉列表必须为空。

int x = 0;
// 对于全局的lambda表达式,不需要捕捉任何变量
// 因为全局变量不用捕捉就可以使用,所以捕捉列表必须为空
auto func1 = []()
{
	++x;
};
// 编译错误
// 无法在lambda表达式中捕捉带有静态存储持续时间的变量,
// 也就是无法捕捉静态区的变量
// auto func2 = [x]()
// {
// 	++x;
// };
// [&x] 这样写也不对
//

默认情况下,lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。

#include <iostream>
using namespace std;

int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象​
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
	{
		// 值捕捉的变量不能修改,引用捕捉的变量可以修改​
		// a++;
		b++;
		int ret = a + b;
		return ret;
	};
	cout << func1() << endl;

	// 传值捕捉本质是一种拷贝,并且被const修饰了​
	// mutable相当于去掉const属性,可以修改了​
	// 但是修改了不会影响外面被捕捉的值,因为是一种拷贝​
	auto func2 = [a, b, c, d]() mutable
	{
		a++;
		b++;
		c++;
		d++;
		return a + b + c + d;
	};
	return 0;
}

1.2.3 捕捉方式

第一种捕捉方式:在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。

例如[x, y, &z] 表示 x 和 y 是值捕捉,z 是引用捕捉。

第二种捕捉方式:在捕捉列表中隐式捕捉,我们在捕捉列表中写一个 "=" 表示隐式值捕捉,在捕捉列表中写一个 "&" 表示隐式引用捕捉编译器会根据函数体内使用了哪些外部变量,就捕捉哪些。编译器不会捕捉全部的外部变量。

第三种捕捉方式:在捕捉列表中混合使用隐式捕捉和显示捕捉,[=, &x] 表示其他变量隐式值捕捉,x 变量显示引用捕捉。[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。

#include <iostream>
using namespace std;

int main()
{
	// 传值捕捉和传引用捕捉
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
	{
		b++;
		int ret = a + b;
		return ret;
	};
	cout << func1() << endl;

	// 隐式值捕捉​
	// 用了哪些变量就捕捉哪些变量​
	auto func2 = [=]
	{
		int ret = a + b + c;
		return ret;
	};
	cout << func2() << endl;

	// 隐式引用捕捉​
	// 用了哪些变量就捕捉哪些变量​
	auto func3 = [&]
	{
		a++;
		c++;
		d++;
	};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;

	// 混合捕捉1​
	auto func4 = [&, a, b]
	{
		c++;
		d++;
		return a + b + c + d;
	};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;

	// 混合捕捉2
	auto func5 = [=, &a, &b]
	{
		a++;
		b++;
		return a + b + c + d;
	};
	func5();
	cout << a << " " << b << " " << c << " " << d << endl;

	return 0;
}

1.3 lambda的应用

在学习lambda表达式之前,我们使用可调用对象只有函数指针和仿函数,函数指针的类型定义起来比较麻烦,仿函数需要定义一个类,也比较麻烦。使用lambda去定义可调用对象,既简单又方便。lambda在很多地方用起来也很好用,比如线程中定义线程的执行函数逻辑,智能指针中定义删除器等。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Goods
{
	string _name;  // 名字​
	double _price; // 价格​
	int _evaluate; // 评价​
	// ...
	Goods(const char *str, double price, int evaluate)
		: _name(str), _price(price), _evaluate(evaluate)
	{
	}
};
struct ComparePriceLess
{
	bool operator()(const Goods &gl, const Goods &gr)
	{
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater
{
	bool operator()(const Goods &gl, const Goods &gr)
	{
		return gl._price > gr._price;
	}
};
int main()
{
	vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
	// 类似这样的场景,我们实现仿函数对象或者函数指针进行传参就很麻烦,
    // 如果代码不说明注释,还需要去查看它的定义,代码不清晰简洁
	// 对于lambda表达式的优势显而易见
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
	sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
		 { return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
		 { return g1._price > g2._price; });
	sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
		 { return g1._evaluate < g2._evaluate; });
	sort(v.begin(), v.end(), [](const Goods &g1, const Goods &g2)
		 { return g1._evaluate > g2._evaluate; });
	return 0;
}

1.4 lambda的原理

闭包类型 = 类

// 对于lambda表达式
auto f = [x](int y)
{
    return x + y;
};

// 编译器在编译时会生成一个类

class __ClosureType
{
private:
    int x;

public:
    __ClosureType(int value)
        : x(value)
    {}

    int operator()(int y) const
    {
        return x + y;
    }
};
// 对于 __ClosureType 这个编译器生成类就是闭包类型

闭包对象 = 用闭包类型定义一个对象

__ClosureType f(x);

// f 就是闭包对象

为什么叫闭包?

因为它把「函数 + 运行环境」封装在一起了。

lambda的本质就是编译器会生成一个对应的仿函数的类。仿函数的类名是编译时按照一定的规则生成的,保证生成不同的lambda对象的类名,lambda参数、返回类型、函数体就是仿函数operator()的参数、返回类型、函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,用来走初始化列表初始化生成的仿函数类的成员变量,对于隐式捕捉,编译器首先检查函数体内使用了哪些外部变量,从而编译时生成对应的成员变量。对于值捕捉的变量默认情况下自带const属性,是因为编译器生成的operator被const修饰,无法修改成员变量。


函数调用

示例:通过汇编层面理解lambda表达式的本质

#include <iostream>
using namespace std;
class Rate
{
public:
	Rate(double rate)
		: _rate(rate)
	{
	}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}

private:
	double _rate;
};

int main()
{
	double rate = 0.49;
	// lambda
	auto r2 = [rate](double money, int year)
	{
		return money * rate * year;
	};
	// 函数对象​
	Rate r1(rate);
	r1(10000, 2);
	
	r2(10000, 2);
	return 0;
}

1.5 lambda与函数指针

很多初学者会认为lambda既然能像函数一样调用,那么它应该就是一个函数指针,实际上并不是,lambda的本质是闭包对象,而函数指针只是一个保存函数地址的指针,两者完全不是同一个东西。

1.5.1 普通函数与函数指针

void func()
{
    cout << "hello" << endl;
}
// 函数名可以退化成函数指针

void (*p)() = func;

p();
func();

// 此时 p() 和 func() 是等价的
// 这里的 p 保存的是函数入口地址

1.5.2 lambda不是函数指针?

auto f = []()
{
    cout << "hello" << endl;
};
// 编译器会推导为一个类
class ClosureType
{
public:
    void operator()() const
    {
        cout << "hello" << endl;
    }
};

f(); 实际上是 f.operator()();
因此lambda是对象,不是函数指针

1.5.3 为什么无捕获 lambda 可以赋值给函数指针?

auto f = []()
{
    cout << "hello" << endl;
};
// 合法
void (*p)() = f;

// 为什么呢?
// 因为无捕获 lambda 没有状态
// 编译器会额外生成一个静态函数

class ClosureType
{
public:
    void operator()() const
    {
        cout << "hello" << endl;
    }

    static void __invoke()
    {
        cout << "hello" << endl;
    }
};
// 然后void (*p)() = f; 等价于 void (*p)() = &ClosureType::__invoke;
// 能够隐式类型转换为函数指针

1.5.4 有捕获 lambda 为什么不行?

int x = 10;

auto f = [x]()
{
    cout << x << endl;
};
// 编译器生成的
class ClosureType
{
private:
    int x;

public:
    ClosureType(int value)
        : x(value)
    {
    }

    void operator()() const
    {
        cout << x << endl;
    }
};
// 这里的 x 已经称为对象的成员变量
// 调用时需要this->x 才能访问
// 因此 f(); == f.operator()(); 需要对象参与
// 而函数指针只有void (*p)() 它只知道函数地址,不知道对象地址
// 因此无法完成调用 void (*p)() = f; 就会编译失败

1.5.5 总结:

Lambda 本质上是一个闭包对象,而函数指针仅保存函数地址。对于无捕获 Lambda,由于其不包含任何状态信息,编译器可以将其转换为对应的函数指针;而有捕获 Lambda 内部保存了成员数据,调用时依赖具体对象,因此无法转换为普通函数指针。

更多推荐