21、匿名函数(C11):lambda表达式的模拟,宏实现
说实话,C语言一直有个让人又爱又恨的地方——它没有真正的匿名函数。C++从C++11开始就有了lambda,Python、JavaScript这些脚本语言更是把lambda玩出了花。但C呢?嗯,标准委员会在C11里也没给这个特性。
不过别急。我们嵌入式工程师最擅长的就是——没有条件,创造条件也要上。今天我就带你用宏来模拟lambda表达式。这招我在好几个项目里都用过,效果还不错。
21.1 什么是匿名函数?为什么我们需要它?
匿名函数,说白了就是没有名字的函数。你想想看,有时候我们只是想把一段逻辑作为参数传给另一个函数,比如排序时的比较逻辑、回调函数等。如果每次都要单独定义一个命名函数,代码会变得很臃肿。
举个例子:
// 传统做法:先定义一个命名函数
int compare_int(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
// 然后才能用
qsort(arr, n, sizeof(int), compare_int);
如果有了匿名函数,我们希望能写成这样(伪代码):
qsort(arr, n, sizeof(int), [](const void* a, const void* b) {
return *(int*)a - *(int*)b;
});
可惜C语言不支持这种语法。但我们可以用宏来模拟,虽然做不到完全一样,但至少能把函数定义和使用写在一起,减少代码跳转。
21.2 核心思路:用宏包装函数定义
我的思路其实很简单——利用宏在编译期生成一个临时函数。具体来说,就是让宏展开成一个static inline函数定义,然后把这个函数的指针传过去。
你可能会问:这不还是命名函数吗?没错,但从代码结构上看,它看起来像匿名函数,而且函数名由宏自动生成,避免了命名冲突。
来看一个最基础的实现:
#define LAMBDA(return_type, body) \
({ \
return_type __lambda_func__ body \
__lambda_func__; \
})
嗯,这个写法其实有问题。GCC的语句表达式(statement expression)扩展允许我们在宏里定义函数,但这不是标准C。我们得换个思路。
21.3 标准C下的实现方案
我个人习惯用宏+静态函数的组合方式。核心思想是:宏定义一个代码块,里面包含函数定义和函数指针的获取。
先看一个能用的版本:
#define LAMBDA(name, return_type, params, body) \
static return_type name##_impl params body \
name##_impl
// 使用示例
void test() {
// 定义一个"匿名"比较函数
int (*cmp)(const void*, const void*) =
LAMBDA(mycmp, int, (const void* a, const void* b), {
return *(int*)a - *(int*)b;
});
int arr[] = {3, 1, 4, 1, 5, 9};
qsort(arr, 6, sizeof(int), cmp);
}
这里有个小技巧:##连接符用来生成唯一函数名。如果你在同一个作用域里定义多个lambda,记得给不同的name参数。我在项目中遇到过一个问题——两个lambda用了同一个名字,结果编译报重复定义。从那以后,我习惯用__LINE__来生成唯一名:
#define LAMBDA_AUTO(return_type, params, body) \
LAMBDA(__lambda_##__LINE__, return_type, params, body)
这样每个lambda都会自动获得一个基于行号的唯一名称,基本不会冲突。
21.4 进阶:支持捕获上下文
真正的lambda还有一个重要特性——捕获外部变量。C语言的宏做不到真正的闭包,但我们可以通过结构体+函数指针来模拟。
来看一个更完整的方案:
// 定义一个"闭包"结构体
typedef struct {
int (*func)(void*, int);
void* context;
} lambda_t;
// 创建闭包的宏
#define LAMBDA_CLOSURE(name, return_type, param_type, body, captured) \
static return_type name##_handler(void* ctx, param_type arg) { \
typeof(captured)* cap = (typeof(captured)*)ctx; \
body \
} \
lambda_t name = { name##_handler, &(captured) }
// 使用示例
void test_closure() {
int factor = 10;
int data = 5;
LAMBDA_CLOSURE(mul, int, int, {
return cap->factor * arg;
}, .factor = factor);
// 调用闭包
int result = mul.func(mul.context, data);
// result = 50
}
这个方案有个限制——只能捕获一个结构体。如果你需要捕获多个变量,可以定义一个结构体来打包。我曾经在一个信号处理项目里用过这个模式,用来实现不同参数下的回调函数,效果还不错。
核心要点:
- 宏展开生成静态函数,函数名自动生成避免冲突
- 用
__LINE__宏保证名称唯一性 - 通过结构体+函数指针模拟闭包捕获
- 本质是语法糖,编译后仍然是普通函数调用
21.5 避坑指南
我曾经踩过的坑:
- 宏展开后的作用域问题:宏里的static函数定义在文件作用域,如果你在循环里多次调用同一个lambda宏,第二次调用会报重复定义。解决方案:用
__COUNTER__或__LINE__生成唯一名。 - 捕获变量的生命周期:模拟闭包时,捕获的变量必须是静态存储期或者堆分配的。如果捕获栈上变量,函数返回后指针就悬空了。我吃过这个亏,调试了一下午才发现是野指针问题。
- 类型安全:宏不检查类型,传错参数类型会导致难以排查的bug。建议在宏内部加一些类型检查的trick,比如用
_Generic做静态断言。
我的建议:
如果你在写嵌入式代码,而且编译器支持GNU扩展,可以考虑用statement expression来实现更优雅的lambda。但如果你追求可移植性,就用我上面介绍的static函数+宏方案。我个人在工业级项目里更倾向于后者——虽然写起来啰嗦一点,但可读性和可维护性更好。
21.6 知识体系图
下面这张图总结了本章的核心知识结构:
21.7 总结
C语言没有原生的lambda表达式,但我们可以用宏来模拟。说白了,这只是一个语法糖——宏在预处理阶段展开成普通的静态函数定义,然后通过函数指针传递。它不会改变C语言的底层机制,但能让代码更紧凑、更易读。
我个人觉得,在嵌入式开发中,这种技巧特别适合用在回调函数注册和算法参数化的场景。比如你要给一个排序算法传不同的比较逻辑,或者给一个定时器注册不同的超时处理函数,用lambda宏可以让代码逻辑更集中,不用在文件里到处找那个只在一处使用的命名函数。
当然,它也有局限性——不能真正捕获变量、类型安全性差、可移植性受限。如果你需要更强大的函数式编程特性,建议考虑C++或者换个语言。但在C的世界里,这已经是我们能做到的最好程度了。
公众号:蓝海资料掘金营,微信deep3321