17. 函数对象(仿函数):让函数“对象化”
函数对象,也叫仿函数(Functor),是 C++ 里一个很有意思的设计。说白了,就是让一个类的实例可以像函数一样被调用。你可能会问:“我有普通函数,为什么还要搞个对象出来?” 嗯,这个问题我当年也困惑过。
我个人习惯把仿函数理解为“带状态的函数”。普通函数是“死”的,你传什么参数,它返回什么结果,中间没有记忆。但仿函数不一样——它是个对象,可以有自己的成员变量,可以保存状态。这一点在实际工程中非常有用。
operator() 的类,其实例就是函数对象。
17.1 函数对象的基本概念
先看一个最简单的例子:
#include <iostream>
class Greeter {
public:
void operator()(const std::string& name) const {
std::cout << "Hello, " << name << "!" << std::endl;
}
};
int main() {
Greeter greet;
greet("Alice"); // 像函数一样调用
greet("Bob");
return 0;
}
你看,greet("Alice") 这个写法,和普通函数调用一模一样。但 greet 其实是个对象。这就是仿函数最直观的样子。
为什么叫“仿函数”?因为它“模仿”了函数的行为。在 STL 里,仿函数大量用于算法中,比如 std::sort 的第三个参数。
17.2 预定义的函数对象
STL 已经帮我们准备了一些常用的仿函数,放在 <functional> 头文件里。我个人最常用的是 std::less 和 std::greater。
| 仿函数 | 作用 | 示例 |
|---|---|---|
std::less<T> |
判断 a < b | less<int>()(3, 5) 返回 true |
std::greater<T> |
判断 a > b | greater<int>()(3, 5) 返回 false |
std::equal_to<T> |
判断 a == b | equal_to<int>()(3, 3) 返回 true |
std::not_equal_to<T> |
判断 a != b | not_equal_to<int>()(3, 5) 返回 true |
std::plus<T> |
返回 a + b | plus<int>()(3, 5) 返回 8 |
std::minus<T> |
返回 a - b | minus<int>()(3, 5) 返回 -2 |
这些预定义的仿函数,说白了就是帮你省去写简单比较逻辑的功夫。比如你想降序排序一个 vector:
#include <algorithm>
#include <vector>
#include <functional>
std::vector<int> vec = {3, 1, 4, 1, 5, 9};
std::sort(vec.begin(), vec.end(), std::greater<int>());
// 结果:9, 5, 4, 3, 1, 1
你想想看,如果没有 std::greater,你得自己写一个比较函数或者 lambda。有了它,一行搞定。
std::greater<>(空尖括号),它会自动推导类型,写起来更简洁。
17.3 自定义仿函数
预定义的仿函数毕竟有限。实际项目中,我们经常需要自定义。我在项目中遇到过这样一个场景:需要根据员工的“绩效分 + 工龄”综合排序。这时候自定义仿函数就派上用场了。
#include <iostream>
#include <vector>
#include <algorithm>
struct Employee {
std::string name;
int score; // 绩效分
int years; // 工龄
};
struct EmployeeComparator {
bool operator()(const Employee& a, const Employee& b) const {
// 先按绩效分降序,相同则按工龄降序
if (a.score != b.score)
return a.score > b.score;
return a.years > b.years;
}
};
int main() {
std::vector<Employee> team = {
{"Alice", 85, 3},
{"Bob", 92, 5},
{"Carol", 85, 7}
};
std::sort(team.begin(), team.end(), EmployeeComparator());
for (const auto& e : team) {
std::cout << e.name << ": " << e.score << "分, " << e.years << "年" << std::endl;
}
return 0;
}
输出:
Bob: 92分, 5年
Carol: 85分, 7年
Alice: 85分, 3年
你看,EmployeeComparator 这个仿函数封装了完整的排序逻辑。如果以后排序规则变了,你只需要改这一个地方,不用到处翻代码。
operator() 最好声明为 const。因为 STL 算法通常要求比较操作不修改对象状态。我曾经因为漏了 const,在编译时遇到一堆看不懂的模板错误……嗯,血的教训。
17.4 Lambda 表达式与仿函数的关系
Lambda 表达式是 C++11 引入的语法糖。说白了,它就是匿名仿函数。编译器会把 lambda 表达式展开成一个匿名的仿函数类。
看这个对比:
// 方式一:自定义仿函数
struct Multiply {
int factor;
Multiply(int f) : factor(f) {}
int operator()(int x) const { return x * factor; }
};
std::vector<int> result1;
std::transform(vec.begin(), vec.end(), std::back_inserter(result1), Multiply(3));
// 方式二:Lambda 表达式
std::vector<int> result2;
std::transform(vec.begin(), vec.end(), std::back_inserter(result2),
[factor = 3](int x) { return x * factor; });
两种写法效果完全一样。但 lambda 更简洁,尤其适合一次性使用的场景。那什么时候该用仿函数呢?我个人总结了几点:
- 逻辑需要复用:如果同一个比较/操作要在多个地方使用,定义成仿函数更合适。
- 逻辑比较复杂:lambda 写长了可读性会下降,仿函数可以拆成多个成员函数。
- 需要继承或组合:仿函数是类,可以继承、可以组合,lambda 做不到。
- 需要调试:仿函数有名字,调试器里看得清楚。lambda 在调试器里通常显示为
__lambda_xxx,很头疼。
我记得有一次排查线上 bug,就是因为 lambda 捕获了某个变量的引用,但那个变量在 lambda 执行前就被销毁了。如果用仿函数,这种问题会更容易发现——因为成员变量的生命周期是明确的。
17.5 知识体系总览
下面这张图帮你理清本章的核心脉络:
17.6 避坑指南与最佳实践
最后,分享几个我这些年积累的经验:
- 仿函数 vs 函数指针:仿函数可以内联,性能更好。函数指针有间接调用开销。在性能敏感的代码里,我优先用仿函数或 lambda。
- 小心捕获引用:lambda 捕获
[&]很方便,但容易造成悬空引用。我建议优先按值捕获,或者显式列出要捕获的变量。 - 命名要清晰:自定义仿函数的名字最好能体现它的用途,比如
ByScoreDescending,而不是MyComparator。代码是写给人看的。 - 善用
std::function:如果你需要在运行时动态决定调用哪个仿函数,可以用std::function来包装。但注意它会有一些运行时开销。
好了,关于函数对象的内容就讲到这里。希望你能在实际项目中找到它的用武之地。
公众号:蓝海资料掘金营,微信 deep3321