5. Lambda与自定义函数对象:仿函数、Lambda与仿函数的对比、何时选择Lambda
好,咱们今天聊点实在的。Lambda 表达式虽然方便,但它并不是 C++ 里唯一能“像函数一样调用”的东西。还有一种老牌技术——仿函数(Functor),也就是重载了 operator() 的类对象。
我个人习惯把仿函数叫做“带状态的函数”。你想想看,普通函数除了参数和全局变量,没法保存自己的“记忆”。但仿函数可以。它是个对象,有成员变量,能记住东西。
5.1 仿函数长什么样?
说白了,仿函数就是一个类,里面重载了 operator()。比如:
class Adder {
public:
Adder(int inc) : increment(inc) {}
int operator()(int x) const {
return x + increment;
}
private:
int increment;
};
// 使用
Adder add5(5);
std::cout << add5(10); // 输出 15
嗯,这里要注意:add5(10) 看起来像函数调用,实际上调用的是 operator()。这就是仿函数名字的由来——模仿函数行为的对象。
我的经验:我在项目中遇到过用仿函数做策略模式的场景。比如一个排序算法,需要根据不同的比较策略来排序。用仿函数可以轻松保存策略参数,比函数指针灵活多了。
5.2 Lambda 与仿函数的对比
Lambda 表达式本质上就是编译器帮你生成的匿名仿函数。但两者在使用上还是有区别的。我整理了一张表,方便你对比:
| 对比维度 | Lambda 表达式 | 仿函数 |
|---|---|---|
| 代码简洁性 | 极高,一行搞定 | 需要定义类,代码量多 |
| 捕获状态 | 通过捕获列表,灵活但有限制 | 成员变量任意,完全控制 |
| 复用性 | 通常一次使用,难以复用 | 可以多次实例化,复用性好 |
| 类型 | 匿名类型,每个 Lambda 不同 | 有明确类型名,可继承 |
| 调试友好度 | 调试器里显示为 lambda |
有类名,容易定位 |
| 性能 | 理论上与仿函数一致 | 理论上与 Lambda 一致 |
为什么会这样?说白了,Lambda 是“快消品”,用完即走。仿函数是“耐用品”,可以反复使用,甚至还能继承和多态。
核心区别:Lambda 是语法糖,仿函数是底层实现。Lambda 能做的,仿函数都能做;仿函数能做的,Lambda 不一定能做得优雅。
5.3 何时选择 Lambda?
我个人总结了三句话:
- 一次性的逻辑——用 Lambda。比如给
std::sort传个比较器,写完就扔,没必要单独定义个类。 - 需要捕获局部变量——用 Lambda。捕获列表
[=]或[&]太方便了,仿函数还得手动传参。 - 代码量敏感——用 Lambda。几行代码能解决的事,别搞个类出来。
// Lambda 版本:简洁
std::vector<int> v = {3, 1, 4, 1, 5};
int threshold = 3;
auto count = std::count_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; });
// 仿函数版本:啰嗦
class GreaterThan {
public:
GreaterThan(int t) : threshold(t) {}
bool operator()(int x) const { return x > threshold; }
private:
int threshold;
};
auto count2 = std::count_if(v.begin(), v.end(), GreaterThan(threshold));
你看,Lambda 版本明显更清爽。我在实际项目中,90% 的场景都用 Lambda 就够了。
5.4 何时选择仿函数?
那什么时候该用仿函数呢?我遇到过这么几种情况:
- 需要多次复用同一个逻辑——仿函数可以定义一次,到处实例化。
- 逻辑复杂,需要多个成员函数辅助——Lambda 只能有一个
operator(),仿函数可以有任意多个辅助方法。 - 需要继承或多态——仿函数可以是类层次结构的一部分,Lambda 不行。
- 需要显式类型名——比如作为模板参数传递时,仿函数有名字,Lambda 是匿名的。
我曾经踩过的坑:有一次我用 Lambda 实现了一个复杂的比较逻辑,后来需要在三个不同的地方复用。结果只能复制粘贴代码,改一处还得改三处。如果当初用仿函数,改一个类定义就行了。所以,复用超过两次,就考虑仿函数。
5.5 知识体系总览
下面这张图帮你理清 Lambda 和仿函数的关系:
这张图的核心逻辑很简单:一次性的、轻量的、捕获局部变量的场景,无脑选 Lambda。需要复用、复杂状态、多态的场景,选仿函数。
5.6 一个实战对比
最后,我拿一个真实项目中的例子来收尾。假设我们要对一个用户列表做过滤,条件是可变的:
// Lambda 版本:适合条件简单且一次性
std::vector<User> users = ...;
int minAge = 18;
auto adults = std::find_if(users.begin(), users.end(),
[minAge](const User& u) { return u.age >= minAge; });
// 仿函数版本:适合条件复杂且可复用
class AgeFilter {
public:
AgeFilter(int min, int max) : minAge(min), maxAge(max) {}
bool operator()(const User& u) const {
return u.age >= minAge && u.age <= maxAge;
}
private:
int minAge;
int maxAge;
};
// 可以多次使用
AgeFilter filter18to60(18, 60);
AgeFilter filter30to40(30, 40);
auto r1 = std::find_if(users.begin(), users.end(), filter18to60);
auto r2 = std::find_if(users.begin(), users.end(), filter30to40);
你看,Lambda 适合“用完即弃”的场景。仿函数适合“定义一次,多处使用”的场景。没有谁绝对好,关键看场景。
我的建议:新手先从 Lambda 入手,用熟了再考虑仿函数。等你遇到“这个 Lambda 我好像在哪见过”的时候,自然就知道该用仿函数了。