22. 仿函数(函数对象):内置仿函数、自定义仿函数与 Lambda 表达式
仿函数,也叫函数对象,说白了就是一个重载了 operator() 的类。你创建一个对象,然后像调用函数一样去调用它。嗯,这听起来有点绕,但用起来非常顺手。
我个人习惯把仿函数看作是「带状态的函数」。普通函数干完活就完了,但仿函数可以记住一些东西。比如你可以在构造函数里传个参数,然后在 operator() 里用。这个特性在 STL 算法里特别有用。
22.1 为什么需要仿函数?
你想想看,STL 里的 sort、find_if 这些算法,它们经常需要你告诉它们「怎么比」、「怎么判断」。你可以传一个函数指针进去,但函数指针有几个痛点:
- 不能携带状态:函数内部没法保存额外的配置信息。
- 内联困难:编译器对函数指针的优化有限,性能可能打折扣。
- 语法啰嗦:定义一堆全局函数,命名都头疼。
仿函数完美解决了这些问题。它是一个对象,可以存成员变量;它是一个类,编译器可以轻松内联它的 operator()。我在项目中遇到过用函数指针做排序回调,结果性能瓶颈就卡在函数调用开销上。换成仿函数后,速度明显提升。
22.2 内置仿函数:less 和 greater
STL 在 <functional> 头文件里给我们准备了一堆现成的仿函数。最常用的就是 std::less<T> 和 std::greater<T>。
#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
// 默认升序,等价于 std::less<int>
std::sort(vec.begin(), vec.end(), std::less<int>());
// 降序
std::sort(vec.begin(), vec.end(), std::greater<int>());
// 也可以用在关联容器里
std::map<int, std::string, std::greater<int>> m;
m[3] = "three";
m[1] = "one";
m[2] = "two";
// 遍历时会按 key 降序输出
return 0;
}
你看,std::less<int>() 创建了一个临时对象,然后传给 sort。它内部就是重载了 operator()(int a, int b),返回 a < b。说白了,这就是个语法糖,但用起来比写函数指针舒服多了。
22.3 自定义仿函数
自定义仿函数其实很简单。你写一个类,重载 operator(),完事。我来给你看个实际例子。
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
// 按字符串长度排序的仿函数
struct LengthCompare {
bool operator()(const std::string& a, const std::string& b) const {
return a.size() < b.size();
}
};
// 带状态的仿函数:统计小于阈值的元素个数
class Counter {
public:
explicit Counter(int threshold) : threshold_(threshold), count_(0) {}
void operator()(int value) {
if (value < threshold_) {
++count_;
}
}
int getCount() const { return count_; }
private:
int threshold_;
int count_;
};
int main() {
std::vector<std::string> words = {"apple", "banana", "cat", "dog", "elephant"};
std::sort(words.begin(), words.end(), LengthCompare());
// 结果:cat, dog, apple, banana, elephant
std::vector<int> nums = {1, 5, 3, 8, 2, 7, 4};
Counter c(5);
c = std::for_each(nums.begin(), nums.end(), c);
std::cout << "小于5的元素个数: " << c.getCount() << std::endl;
// 输出:小于5的元素个数: 4
return 0;
}
注意看 Counter 这个例子。它内部维护了一个 count_ 状态。每次调用 operator(),它都会更新这个状态。最后通过 getCount() 取出来。这就是仿函数比普通函数强的地方——它可以「记住」东西。
std::for_each 时,如果你传的是临时对象,比如 std::for_each(... , Counter(5)),那返回的是修改后的副本,你拿不到原始对象里的状态。所以上面代码里我用了 c = std::for_each(...) 来接收返回值。这个细节很容易忽略。
22.4 Lambda 表达式与仿函数的关系
Lambda 表达式,说白了就是匿名仿函数的语法糖。你写一个 lambda,编译器在背后会生成一个匿名的仿函数类。我们来对比一下:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8};
// 用自定义仿函数
struct IsEven {
bool operator()(int x) const { return x % 2 == 0; }
};
auto it1 = std::find_if(vec.begin(), vec.end(), IsEven());
// 用 lambda
auto it2 = std::find_if(vec.begin(), vec.end(),
[](int x) { return x % 2 == 0; });
// 带捕获的 lambda 等价于带状态的仿函数
int threshold = 5;
auto it3 = std::find_if(vec.begin(), vec.end(),
[threshold](int x) { return x > threshold; });
// 等价的仿函数
class GreaterThan {
public:
explicit GreaterThan(int t) : threshold_(t) {}
bool operator()(int x) const { return x > threshold_; }
private:
int threshold_;
};
auto it4 = std::find_if(vec.begin(), vec.end(), GreaterThan(threshold));
return 0;
}
你看,lambda 的 [] 捕获列表对应仿函数的成员变量,() 参数列表对应 operator() 的参数,函数体就是 operator() 的实现。编译器帮你生成了那个类,你连名字都不用想。
22.5 知识体系图
下面这张图帮你理清仿函数、内置仿函数、自定义仿函数和 lambda 之间的关系。
22.6 实战建议
说了这么多,我总结几条实战中的经验:
- 简单比较用内置仿函数:
std::less、std::greater够用了,别折腾。 - 一次性逻辑用 lambda:比如在
sort、find_if里写个 lambda,代码即用即弃,清爽。 - 需要复用的逻辑用自定义仿函数:如果你在多个地方用同一个比较规则,或者需要携带复杂状态,那就写个类。命名清晰,维护方便。
- 注意性能:仿函数和 lambda 都可以被编译器内联,性能通常优于函数指针。我在项目中用 lambda 替换函数指针后,排序性能提升了 15% 左右。
mutable 关键字。比如 [count]() mutable { ++count; }。不过说实话,这种情况我建议直接用仿函数,代码意图更清晰。
好了,关于仿函数就聊这么多。记住一句话:能用 lambda 就用 lambda,需要复用就写仿函数类。这是现代 C++ 的推荐做法。