C++ lambda表达式转成普通的函数指针
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表达式是最方便的。
- 无捕获
如果在lambda表达式内不需要捕获外部变量,如下所示:
1int main(){
2 int m = 1;
3 Test t;
4 t.foreach(
5 [](int i) {
6 cout << i << "\n";
7 });
8}
这样写是没问题的,编译器可以编译通过。
- 有捕获 如果是有捕获的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
- 原文作者:Witton
- 原文链接:https://wittonbell.github.io/posts/2025/2025-01-17-C++-lambda表达式转成普通的函数指针/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。