C++ 在 C++11版本中引入了lambda表达式,在C++11之后的版本也有对其进行改进,这里就不再赘述了。

一、lambda表达式的使用

C++ lambda表达式有下面几种使用情况:

1. 无捕获

无捕获即在表达式内不可使用表达式外的变量,示例:

1void foo() {
2	int n = 10;
3	[]() {
4		// 这里不能使用变量n
5		cout << "lambda\n";
6	}();
7}

2. 值捕获

值捕获即在表达式内可以使用外部变量,值捕获又分为全值捕获特定值捕获,根据是否可以临时修改外部变量的值,又分为不可临时修改外部变量的值可临时修改外部变量的值

临时修改外部变量的值,即在表达式函数内可以使用并修改外部变量的值,但是出了表达式函数的作用域则恢复原来的外部变量的值

2.1 不可临时修改外部变量的值

在捕获列表中使用=时即为全值捕获,示例:

1void foo() {
2	int m = 1;
3	int n = 10;
4	[=]() {
5		// 这里可以使用外部的所有变量
6		cout << "lambda\n";
7	}();
8}

在捕获列表中书写特定变量名时即为特定值捕获,示例:

1void foo() {
2	int m = 1;
3	int n = 10;
4	int k = 20;
5	[m, n]() {
6		// 这里可以使用外部变量m, n,但是不能使用变量k
7		cout << "lambda\n";
8	}();
9}

2.2 可临时修改外部变量的值

如果想要临时使用并修改外部变量,而不影响外部变量在lambda表达式外的值,则可以在lambda表达式函数后使用 mutable 关键字,示例:

 1void foo() {
 2	int m = 1;
 3	int n = 10;
 4	[=]() mutable {
 5		// 这里可以使用并修改外部的所有变量,但不影响变量在lambda表达式外的值
 6		// 这里修改了m的值为2,但出了lambda表达式函数体的作用域,m的值被恢复
 7		m *= 2;
 8		cout << "lambda\n";
 9	}();
10	// m的值还是为1
11	[n]() mutable {
12		// 这里只能所有变量n,也可以修改n,但不影响lambda表达式外n的值
13		cout << "lambda\n";
14	}();
15}

3. 引用捕获

引用捕获即在表达式内可以使用外部变量,并且可以修改变量的值并影响lambda表达式外的值,引用捕获又分为全引用捕获特定引用捕获

在捕获列表中使用&时即为全引用捕获,示例:

1void foo() {
2	int m = 1;
3	int n = 10;
4	[&]() {
5		// 这里可以使用外部的所有变量
6		cout << "lambda\n";
7	}();
8}

在捕获列表中书写特定变量名,并且变量名前有&符号,即为特定引用捕获,示例:

1void foo() {
2	int m = 1;
3	int n = 10;
4	int k = 20;
5	[&m, &n]() {
6		// 这里可以使用并修改外部变量m, n,但是不能使用变量k
7		cout << "lambda\n";
8	}();
9}

二、lambda表达式的原理

其实C++的lambda表达式是一种函数对象,即是一个重载了()操作符的类对象,比如lambda表达式:

1[](int i) {
2	cout << "lambda\n";
3}

编译器就会自动生成类似的下面的一个类,类名唯一,且重载了operator()操作符:

1class __lambda_1
2{
3public:
4	inline void operator()(int i) const	{
5		cout << "lambda\n";
6	}
7};

需要注意的是编译器默认生成的是带const修饰符的operator()成员函数,意味着不能修改lambda类对象成员变量的值(引用除外,引用捕获就是使用的引用)。

有一个网站 https://cppinsights.io/可以直观地看到C++内部可能的实现,前面的lambda表达式实际为类似这样的类:

 1class __lambda_7_3
 2{
 3  public: 
 4  inline /*constexpr */ void operator()(int i) const
 5  {
 6    std::operator<<(std::cout, "lambda\n");
 7  }
 8  
 9  using retType_7_3 = void (*)(int);
10  inline constexpr operator retType_7_3 () const noexcept
11  {
12    return __invoke;
13  };
14  
15  private: 
16  static inline /*constexpr */ void __invoke(int i)
17  {
18    __lambda_7_3{}.operator()(i);
19  }
20}

1. 无捕获变量

前面的示例就是无捕获的情况。

2. 有捕获变量

既然lambda表达式是一个类对象,那它就可以有自己的变量,用于将捕获的变量保存起来,供表达式函数体使用。

比如:

1#include <iostream>
2using namespace std;
3int main() {
4  int m = 1;
5  [=](int i) {
6	cout << m << "lambda\n";
7  }(1);
8}

编译器可能会生成类似的代码:

 1#include <iostream>
 2using namespace std;
 3int main() {
 4  int m = 1;
 5  class __lambda_8_3 {
 6  public: 
 7    inline /*constexpr */ void operator()(int i) const {
 8      std::operator<<(std::cout.operator<<(m), "lambda\n");
 9    }
10  private: 
11    int m;
12  public:
13    __lambda_8_3(int & _m) : m{_m}
14    {}
15  } __lambda_8_3{m};
16  __lambda_8_3.operator()(1);
17  return 0;
18}

由于是值捕获外部变量m,所以在生成的lambda表达式类时,有一个私有成员变量m,在构造函数中要求外部传入值来初始化变量m,在初始化时是复制值,不是引用。

这是值捕获的情况,再看看引用捕获的情况:

1#include <iostream>
2using namespace std;
3int main() {
4  int m = 1;
5  [&](int i) {
6    m *= 2;
7	cout << m << "lambda\n";
8  }(1);
9}

可能会生成类似下面的代码:

 1#include <iostream>
 2using namespace std;
 3int main() {
 4  int m = 1;
 5  class __lambda_6_3 {
 6  public: 
 7    inline /*constexpr */ void operator()(int i) const {
 8      m = m * 2;
 9      std::operator<<(std::cout.operator<<(m), "lambda\n");
10    }
11  private: 
12    int & m;
13  public:
14    __lambda_6_3(int & _m) : m{_m}
15    {}
16  } __lambda_6_3{m};
17  __lambda_6_3.operator()(1);
18  return 0;
19}

可以看到,由于是引用捕获,lambda表达式类生成了一个引用成员变量m,在构造函数中要求外部传入引用值来初始化变量m,由于变量m是一个引用,所以初始化时是直接使用的引用。

3. 带mutable关键字的lambda表达式

前面说了,在默认情况下,编译器生成的都是带const的修饰符的operator()成员函数,如果lambda表达式使用了mutable关键字,则编译器会将const去掉。

1#include <iostream>
2using namespace std;
3int main() {
4  int m = 1;
5  [=](int i) mutable {
6    m *= 2;
7	cout << m << "lambda\n";
8  }(1);
9}

可能会生成类似下面的代码:

 1#include <iostream>
 2using namespace std;
 3int main() {
 4  int m = 1;
 5  class __lambda_9_3 {
 6  public: 
 7    inline /*constexpr */ void operator()(int i) {
 8      m = m * 2;
 9      std::operator<<(std::cout.operator<<(m), "lambda\n");
10    }
11  private: 
12    int m;
13  public:
14    __lambda_9_3(int & _m) : m{_m}
15    {}
16  } __lambda_9_3{m};
17  __lambda_9_3.operator()(1);
18  return 0;
19}

三、lambda表达式转成普通的函数指针

假设有一个类如下所示:

1class Test {
2public:
3  void foreach(void (*fn)(int k)) {
4    for(int i = 0; i < 10; ++i) {
5      fn(i);
6    }
7  }
8};

foreach函数需要一个普通的函数指针参数,我们在使用的时候直接使用lambda表达式是最方便的。

  1. 无捕获

如果在lambda表达式内不需要捕获外部变量,如下所示:

1int main(){
2  int m = 1;
3  Test t;
4  t.foreach(
5  [](int i) {
6	cout << i << "\n";
7  });
8}

这样写是没问题的,编译器可以编译通过。

  1. 有捕获 如果是有捕获的lambda表达式是否也可以呢?
1int main(){
2  int m = 1;
3  Test t;
4  t.foreach(
5  [=](int i) {
6	cout << i << "\n";
7  });
8}

答案是否定的,编译时,VS2022会报错:

1error C2664: “void Test::foreach(void (__cdecl *)(int))”: 无法将参数 1 从“main::<lambda_1>”转换为“void (__cdecl *)(int)

为什么呢?

仔细对比无捕获生成的lambda表达式类有捕获生成的lambda表达式类有何不同便知道了。关键在于无捕获的lambda表达式类在生成时,生成了一个类型转换函数,类似下面这样:

 1  using retType_7_3 = void (*)(int);
 2  inline constexpr operator retType_7_3 () const noexcept
 3  {
 4    return __invoke;
 5  };
 6  private: 
 7  static inline /*constexpr */ void __invoke(int i)
 8  {
 9    __lambda_7_3{}.operator()(i);
10  }

在这里插入图片描述

有捕获生成的lambda表达式类则没有,可以对比前面的内容。也就是说无捕获lambda表达式可以通过编译器自动生成的类型转换函数自动进行类型转换,而有捕获lambda表达式由于编译器没有自动生成类型转换函数,所以无法进行转换

既然编译器没有自动生成类型转换函数,只有手动转换了,在stackoverflow上有段非常有用的转换代码,笔者做点小修改并添加了注释,源码如下:

 1template <class L, class R, class... Args>
 2static auto impl_impl(L&& l) {
 3	static_assert(!std::is_same<L, std::function<R(Args...)>>::value,
 4		"Only lambdas are supported, it is unsafe to use std::function or other non-lambda callables");
 5
 6    // 这里将lambda表达式类对象保存下来,设置为静态变量,
 7    // 后面的新的lambda表达式才能访问到
 8	static L lambda_s = std::move(l);
 9	// 返回一个新的lambda表达式
10	return [](Args... args) -> R {
11		// 调用保存的lambda表达式类对象的`operator()`方法
12		return lambda_s(args...);
13	};
14}
15
16template <class L>
17struct to_f_impl : public to_f_impl<decltype(&L::operator())>{
18// 这里先定义一个模板,只是确定模板参数用,方便后面的模板特化
19// 模板参数即为:decltype(&L::operator()),
20// 即根据L的`operator()`方法来确定,
21// 而一个类的`operator()`方法格式为:
22// ReturnType ClassType::operator()(ArgType1, ArgType2, ... ArgTypeN)
23// 使用模板来表示则为:
24// template <typename ReturnType, typename ClassType, typename... ArgTypes>
25};
26
27// 特化前面的模板,编译器生成lambda表达式的`operator()`方法默认是带`const`的
28template <class ClassType, class R, class... Args>
29struct to_f_impl<R(ClassType::*)(Args...) const>{
30    // 这里还需要带上lambda表达式类的类型L,方便传递lambda表达式类对象
31	template <class L>
32	static auto impl(L&& l) {
33		return impl_impl<L, R, Args...>(std::move(l));
34	}
35};
36
37// 特化前面的模板类,如果lambda表达式显式设置了mutable关键字,
38// 则编译器生成不带`const`的`operator()`方法
39template <class ClassType, class R, class... Args>
40struct to_f_impl<R(ClassType::*)(Args...)> {
41    // 这里还需要带上lambda表达式类的类型L,方便传递lambda表达式类对象
42	template <class L>
43	static auto impl(L&& l) {
44		return impl_impl<L, R, Args...>(std::move(l));
45	}
46};
47
48// 这是一个最后的封装API,只需要把lambda表达式传进来即可
49// 它调用前面的模板进行lambda表达式的返回值,调用参数类型解析
50template <class L>
51static auto to_f(L&& l) {
52	return to_f_impl<L>::impl(std::move(l));
53}

用法:

1int main(){
2  int m = 1;
3  Test t;
4  t.foreach(to_f([=](int i) {
5	cout << i << "\n";
6  }));
7}

如果对你有帮助,欢迎点赞收藏!

参考: https://stackoverflow.com/questions/28746744/passing-capturing-lambda-as-function-pointer https://stackoverflow.com/questions/28746744/passing-capturing-lambda-as-function-pointer/73177293#73177293 https://segmentfault.com/q/1010000042691339 https://cloud.tencent.com/developer/article/2220354