仿函数与适配器:让C++算法“活”起来
各位,今天我们来聊聊仿函数和适配器。说实话,这两个东西刚接触时有点绕,但用顺了之后,你会发现它们就像瑞士军刀——小巧、灵活、关键时刻能救命。
1. 仿函数:长得像函数,其实是个对象
仿函数(Functor),说白了就是重载了 operator() 的类。你想想看,一个对象能像函数一样被调用,是不是很神奇?
核心思想:用对象保存状态,用调用运算符执行逻辑。
// 一个简单的仿函数
struct Adder {
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Adder add;
int result = add(3, 4); // 像函数一样调用
// result == 7
return 0;
}
我在项目中遇到过这样的场景:需要给 std::sort 传一个比较器,但比较逻辑依赖于某个运行时参数。普通函数做不到,lambda 当时还没普及,仿函数就成了救星。
我的习惯:如果逻辑简单且不需要状态,优先用 lambda;如果需要保存状态或复用,就用仿函数。
2. 函数适配器:给函数“穿衣服”
适配器(Adapter)的作用,就是改变一个函数的行为。C++ 标准库提供了几个经典的适配器:std::bind、std::not_fn、std::function。
2.1 std::bind:绑定参数,生成新函数
std::bind 可以固定某些参数,生成一个参数更少的可调用对象。嗯,这里要注意:占位符 std::placeholders::_1 表示“这个参数留给调用时再传”。
#include <functional>
bool isGreater(int a, int b) {
return a > b;
}
int main() {
using namespace std::placeholders;
// 绑定第二个参数为 10,生成一个一元谓词
auto isGreaterThan10 = std::bind(isGreater, _1, 10);
std::vector<int> data = {5, 12, 3, 18, 7};
int count = std::count_if(data.begin(), data.end(), isGreaterThan10);
// count == 2 (12 和 18)
return 0;
}
我曾经踩过的坑:bind 绑定成员函数时,第一个参数必须是对象指针或智能指针。如果传了临时对象,生命周期一结束就悬空了。后来我改用 lambda 或 std::mem_fn 才彻底解决。
2.2 std::not_fn:逻辑取反
这个适配器简单粗暴——把谓词的结果取反。C++17 引入,替代了老旧的 std::not1 和 std::not2。
auto isEven = [](int x) { return x % 2 == 0; };
auto isOdd = std::not_fn(isEven);
std::vector<int> nums = {1,2,3,4,5};
int oddCount = std::count_if(nums.begin(), nums.end(), isOdd);
// oddCount == 3
2.3 std::function:万能函数包装器
std::function 可以存储任何可调用对象——函数指针、lambda、仿函数、bind 表达式。说白了,它就是一个类型擦除的容器。
#include <functional>
void hello() { std::cout << "Hello\n"; }
int main() {
std::function<void()> func;
func = hello; // 函数指针
func(); // 输出 Hello
func = []() { std::cout << "Lambda\n"; };
func(); // 输出 Lambda
auto bound = std::bind(hello);
func = bound; // bind 表达式
func(); // 输出 Hello
return 0;
}
性能提醒:std::function 有轻微开销(虚函数调用或小对象优化)。如果性能敏感,尽量用 auto 或模板。我一般在回调接口、事件系统里用它,日常算法中尽量不用。
3. 实战:用适配器改造排序
来看一个综合例子。假设你需要对一个 std::vector<std::string> 按字符串长度降序排序,但长度比较逻辑需要复用。
#include <algorithm>
#include <functional>
#include <vector>
#include <string>
bool shorter(const std::string& a, const std::string& b) {
return a.size() < b.size();
}
int main() {
std::vector<std::string> words = {"apple", "banana", "cherry", "date"};
// 用 not_fn 实现降序
std::sort(words.begin(), words.end(),
std::not_fn(std::function<bool(const std::string&, const std::string&)>(shorter)));
// 或者用 bind 绑定参数顺序
using namespace std::placeholders;
std::sort(words.begin(), words.end(),
std::bind(shorter, _2, _1)); // 交换参数实现降序
// 输出: banana, cherry, apple, date
return 0;
}
个人经验:实际项目中我很少用 bind 做参数交换,因为可读性差。更推荐 lambda:[](const auto& a, const auto& b) { return a.size() > b.size(); }。但 bind 在泛型代码中仍有价值,比如需要将成员函数适配成普通函数时。
4. 知识体系图
下面这张图帮你理清仿函数和适配器的关系:
5. 避坑指南与最佳实践
- bind 的占位符别搞混:
_1是调用时的第一个参数,不是 bind 时的第一个参数。我见过有人把_1和_2写反,排查了半天。 - std::function 的构造开销:每次赋值都会拷贝或移动可调用对象。如果对象很大(比如捕获了大量数据的 lambda),性能会下降。建议用
std::ref包装引用。 - not_fn 与老式适配器:C++17 之后请用
std::not_fn,不要再用std::not1和std::not2。后者需要定义argument_type,麻烦且易错。 - 仿函数 vs lambda:如果只需要一次性的简单逻辑,lambda 更简洁。如果需要多次复用且带状态,仿函数更清晰。
我的个人习惯:在头文件中定义仿函数,在源文件中用 lambda。这样接口清晰,实现灵活。
6. 总结
仿函数和适配器是 C++ 泛型编程的基石。仿函数让对象可调用,适配器让函数可组合。说白了,它们就是让代码更灵活、更可复用的工具。你想想看,没有它们,std::sort 怎么支持自定义排序?回调系统怎么实现?
嗯,今天就到这里。记住:多用 lambda,少用 bind,必要时用仿函数。这样写出来的代码,别人看得懂,自己也维护得了。