4. 函数对象的实际应用:作为谓词(Predicate)用于 std::sort, std::find_if
好了,咱们今天聊点实在的。函数对象最经典的应用场景,就是作为谓词(Predicate),配合标准库算法干活。说白了,谓词就是一个返回布尔值的可调用对象,用来描述“某个条件是否成立”。
我个人习惯把谓词分成两类:一元谓词(接收一个参数)和二元谓词(接收两个参数)。前者常用于查找、计数,后者常用于排序、比较。你想想看,std::sort 要排序,总得知道“谁在前谁在后”吧?std::find_if 要查找,总得知道“什么样的元素算符合条件”吧?这些判断逻辑,就是谓词来定义的。
4.1 用函数对象做排序规则
先看 std::sort。默认是按升序排,但如果你想要降序,或者按某种自定义规则排,就得传一个二元谓词进去。
我记得刚入行时,有个同事直接在 sort 里传了个普通函数指针,也能跑。但后来需求变了,排序规则需要带状态——比如“按某个阈值动态调整比较逻辑”。普通函数就抓瞎了,函数对象却能轻松搞定。
#include <iostream>
#include <vector>
#include <algorithm>
// 函数对象:降序比较器
struct Descending {
bool operator()(int a, int b) const {
return a > b;
}
};
int main() {
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};
// 使用函数对象作为谓词
std::sort(data.begin(), data.end(), Descending());
for (int v : data) std::cout << v << " ";
// 输出:9 6 5 4 3 2 1 1
return 0;
}
这里 Descending() 创建了一个临时函数对象,传给 sort。它内部重载了 operator(),每次比较两个元素,返回 true 表示 a 应该排在 b 前面。
4.2 带状态的谓词:动态阈值比较
来看一个我项目中真实遇到的场景。当时需要根据用户输入的阈值,对一组数据进行“接近阈值优先”的排序。阈值是运行时才能确定的,普通函数做不到,但函数对象可以。
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
// 带状态的函数对象:按与阈值的距离排序
struct CloserToThreshold {
int threshold;
CloserToThreshold(int t) : threshold(t) {}
bool operator()(int a, int b) const {
return std::abs(a - threshold) < std::abs(b - threshold);
}
};
int main() {
std::vector<int> scores = {45, 78, 90, 62, 55, 81};
int userThreshold = 70;
std::sort(scores.begin(), scores.end(),
CloserToThreshold(userThreshold));
// 输出:78 62 81 55 90 45
// 越接近70的越靠前
for (int v : scores) std::cout << v << " ";
return 0;
}
你看,CloserToThreshold 内部保存了 threshold 这个状态。每次比较时,它计算两个元素与阈值的距离,距离小的排前面。这就是函数对象比普通函数灵活的地方——它可以“记住”东西。
4.3 用函数对象做查找条件
std::find_if 接收一元谓词,返回第一个使谓词为 true 的元素的迭代器。这个场景下,函数对象同样可以携带状态。
举个例子:查找第一个大于某个阈值的元素。阈值是动态的,用函数对象封装起来很自然。
#include <iostream>
#include <vector>
#include <algorithm>
struct GreaterThan {
int limit;
GreaterThan(int l) : limit(l) {}
bool operator()(int value) const {
return value > limit;
}
};
int main() {
std::vector<int> data = {10, 25, 30, 45, 50};
int threshold = 28;
auto it = std::find_if(data.begin(), data.end(),
GreaterThan(threshold));
if (it != data.end()) {
std::cout << "第一个大于 " << threshold
<< " 的元素是: " << *it << std::endl;
// 输出:第一个大于 28 的元素是: 30
}
return 0;
}
这里 GreaterThan(28) 创建了一个函数对象,它内部保存了 limit=28。find_if 遍历容器时,对每个元素调用 operator(),一旦返回 true 就停下来。
4.4 组合多个条件的谓词
实际项目中,查找条件往往不止一个。比如“查找第一个在某个区间内的元素”。你可以把多个条件封装到一个函数对象里。
#include <iostream>
#include <vector>
#include <algorithm>
struct InRange {
int low, high;
InRange(int l, int h) : low(l), high(h) {}
bool operator()(int value) const {
return value >= low && value <= high;
}
};
int main() {
std::vector<int> data = {5, 12, 18, 25, 30, 8};
auto it = std::find_if(data.begin(), data.end(),
InRange(10, 20));
if (it != data.end()) {
std::cout << "区间 [10, 20] 内的第一个元素: "
<< *it << std::endl;
// 输出:12
}
return 0;
}
你看,InRange 把 low 和 high 两个状态都封装进去了。这种写法比 lambda 更直观——尤其是当条件逻辑复杂时,函数对象可以起个有意义的名字,代码自文档化。
- 谓词就是返回 bool 的可调用对象,用于描述条件
- std::sort 用二元谓词定义排序规则
- std::find_if 用一元谓词定义查找条件
- 函数对象可以携带状态,比普通函数更灵活
- 谓词必须满足严格弱序,否则算法行为未定义
4.5 知识体系图
下面这张图帮你理清函数对象作为谓词的核心脉络:
4.6 避坑指南
最后分享几个我踩过的坑:
- 谓词必须是纯函数吗? 不一定,但最好别在谓词里修改外部状态。std::sort 可能会多次比较同一对元素,如果你的谓词有副作用(比如计数),结果会乱套。我曾经在谓词里加了个打印语句,结果输出量是预期的三倍……
- 函数对象传值还是传引用? 标准库算法默认按值传递函数对象。如果你的函数对象很大(比如内部有个大数组),记得用
std::ref包装一下,避免拷贝开销。 - lambda 和函数对象怎么选? 简单逻辑用 lambda,复杂逻辑(尤其是需要多个成员函数辅助时)用函数对象。我个人习惯:超过 5 行的逻辑就写成函数对象,方便单元测试。
好了,这一章就到这里。函数对象作为谓词,说白了就是“把判断逻辑封装成一个对象,扔给算法用”。你掌握了这个思路,以后遇到任何需要自定义规则的算法,都能游刃有余。