第27章:函数对象与适配器——让C++更灵活
说实话,函数对象这个概念,我刚开始学C++时觉得挺鸡肋的。不就是个重载了operator()的类吗?用得着单独拿出来讲?直到我在一个大型交易系统中,需要把不同的排序策略动态传递给一个容器……嗯,那时候我才真正体会到函数对象的威力。
27.1 函数对象(仿函数)
函数对象,也叫仿函数。说白了,就是一个类,但它可以像函数一样被调用。
class Adder {
public:
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Adder add;
int result = add(3, 5); // 像函数一样调用
std::cout << result; // 输出 8
return 0;
}
你可能会问:直接用函数不就行了?为什么非要搞个类?
原因有三:
- 可以携带状态——函数对象内部可以保存成员变量
- 可以被内联——编译器更容易优化
- 可以作为模板参数——这是普通函数指针做不到的
核心要点:函数对象本质上是一个类实例,但它通过operator()获得了“可调用”的能力。这比函数指针更灵活,比lambda更早出现。
27.2 std::function——万能函数包装器
我在项目中遇到过这样一个场景:需要把回调函数存到一个容器里,但回调函数的类型各不相同。有的返回void,有的返回int,参数个数也不一样。这时候std::function就派上用场了。
#include <functional>
#include <iostream>
void hello() {
std::cout << "Hello, World!" << std::endl;
}
int add(int a, int b) {
return a + b;
}
int main() {
// 存储普通函数
std::function<void()> f1 = hello;
f1(); // 输出 Hello, World!
// 存储lambda
std::function<int(int, int)> f2 = [](int x, int y) { return x * y; };
std::cout << f2(3, 4); // 输出 12
// 存储函数对象
std::function<int(int, int)> f3 = add;
std::cout << f3(10, 20); // 输出 30
return 0;
}
个人建议:能用auto的地方尽量用auto,别滥用std::function。它内部有类型擦除的开销,性能敏感的场景下要小心。我见过有人把std::function用在每秒调用百万次的热路径上,结果性能直接腰斩。
27.3 std::bind——参数绑定利器
std::bind可以“预绑定”函数的某些参数,生成一个新的可调用对象。说白了,就是给函数“打个折”,固定住一部分参数。
#include <functional>
#include <iostream>
void print(int a, int b, int c) {
std::cout << a << ", " << b << ", " << c << std::endl;
}
int main() {
// 绑定所有参数
auto f1 = std::bind(print, 1, 2, 3);
f1(); // 输出 1, 2, 3
// 使用占位符
auto f2 = std::bind(print, std::placeholders::_1, 100, std::placeholders::_2);
f2(50, 200); // 输出 50, 100, 200
// 改变参数顺序
auto f3 = std::bind(print, std::placeholders::_3, std::placeholders::_1, std::placeholders::_2);
f3(10, 20, 30); // 输出 30, 10, 20
return 0;
}
我曾经在重构一个旧项目时,遇到一个函数有6个参数,但调用方每次都只传同样的前3个参数。用std::bind一包装,代码瞬间清爽了。
注意:C++17之后,lambda表达式基本可以替代std::bind。我个人更推荐用lambda,可读性更好。但如果你在维护老代码,还是得看懂std::bind。
27.4 适配器:stack、queue、priority_queue
适配器,说白了就是“套壳”。它不自己管理数据,而是把底层容器包装一下,只暴露特定的接口。
27.4.1 stack——后进先出
#include <stack>
#include <iostream>
int main() {
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
while (!s.empty()) {
std::cout << s.top() << " "; // 输出 3 2 1
s.pop();
}
return 0;
}
默认底层容器是deque,你也可以换成vector或list:
std::stack<int, std::vector<int>> s1;
std::stack<int, std::list<int>> s2;
27.4.2 queue——先进先出
#include <queue>
int main() {
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
while (!q.empty()) {
std::cout << q.front() << " "; // 输出 1 2 3
q.pop();
}
return 0;
}
默认底层容器也是deque。为什么?因为deque在两端插入删除都是O(1),完美匹配queue的需求。
27.4.3 priority_queue——优先级队列
这个有点意思。它内部是一个堆结构,每次top()返回的都是最大(或最小)的元素。
#include <queue>
#include <vector>
#include <iostream>
int main() {
// 默认是大顶堆
std::priority_queue<int> pq;
pq.push(3);
pq.push(1);
pq.push(4);
pq.push(1);
pq.push(5);
while (!pq.empty()) {
std::cout << pq.top() << " "; // 输出 5 4 3 1 1
pq.pop();
}
return 0;
}
想要小顶堆?用std::greater:
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
避坑指南:我曾经在项目中用priority_queue做任务调度,结果发现自定义类型的比较器写错了,导致高优先级任务一直被压在最底下。调试了一下午才发现是operator<的方向搞反了。记住:默认是std::less,也就是大顶堆。
27.5 知识体系总览
下面这张图帮你理清本章的核心脉络:
27.6 综合示例:用适配器实现任务调度
最后,我把今天讲的东西串起来,写一个简单的任务调度器:
#include <iostream>
#include <queue>
#include <functional>
#include <string>
struct Task {
int priority;
std::string name;
std::function<void()> action;
bool operator<(const Task& other) const {
return priority < other.priority; // 优先级高的先执行
}
};
int main() {
std::priority_queue<Task> scheduler;
scheduler.push({1, "低优先级任务", []() {
std::cout << "执行低优先级任务" << std::endl;
}});
scheduler.push({10, "高优先级任务", []() {
std::cout << "执行高优先级任务" << std::endl;
}});
scheduler.push({5, "中优先级任务", []() {
std::cout << "执行中优先级任务" << std::endl;
}});
while (!scheduler.empty()) {
auto task = scheduler.top();
std::cout << "正在执行: " << task.name << std::endl;
task.action();
scheduler.pop();
}
return 0;
}
输出结果:
正在执行: 高优先级任务
执行高优先级任务
正在执行: 中优先级任务
执行中优先级任务
正在执行: 低优先级任务
执行低优先级任务
你看,priority_queue配合std::function,一个简单的任务调度器就出来了。这就是函数对象和适配器组合的威力。
最后说一句:函数对象和适配器,说白了就是C++给你提供的“乐高积木”。单个看都不复杂,但组合起来能解决很多实际问题。多写、多用,慢慢就熟练了。
公众号:蓝海资料掘金营,微信deep3321