3. 标准库中的函数对象:std::plus, std::minus, std::greater, std::less 等。
说实话,刚学C++那会儿,我对这些标准库里的函数对象是有点不屑的。不就是个加法吗?我写个 a + b 不就完了,干嘛还要搞个 std::plus<int>()(a, b) 这么啰嗦?
后来我才明白,是我太年轻了。这些不起眼的小工具,其实是C++泛型编程的基石之一。它们让算法变得极其灵活,就像乐高积木里的标准接口——你不需要关心里面是什么,只要知道它能干什么。
operator() 的类。标准库提供了一组预定义的函数对象,覆盖了常见的算术、比较、逻辑运算。
3.1 算术运算函数对象
标准库在 <functional> 头文件里,给我们准备了一套算术运算的"积木":
| 函数对象 | 运算 | 等价表达式 |
|---|---|---|
std::plus<T> |
加法 | a + b |
std::minus<T> |
减法 | a - b |
std::multiplies<T> |
乘法 | a * b |
std::divides<T> |
除法 | a / b |
std::modulus<T> |
取模 | a % b |
std::negate<T> |
取反 | -a |
你看,它们都是一元或二元函数。用起来很简单:
#include <iostream>
#include <functional>
int main() {
std::plus<int> add;
std::cout << add(3, 5) << std::endl; // 输出 8
std::negate<double> neg;
std::cout << neg(3.14) << std::endl; // 输出 -3.14
return 0;
}
嗯,单独看确实有点傻。但把它们传给算法,威力就出来了。
3.2 比较运算函数对象
比较运算的函数对象,我用的频率其实更高。尤其是排序和查找的时候:
| 函数对象 | 运算 | 等价表达式 |
|---|---|---|
std::equal_to<T> |
等于 | a == b |
std::not_equal_to<T> |
不等于 | a != b |
std::greater<T> |
大于 | a > b |
std::less<T> |
小于 | a < b |
std::greater_equal<T> |
大于等于 | a >= b |
std::less_equal<T> |
小于等于 | a <= b |
举个例子,你想让 std::sort 降序排列?不用写lambda,直接传 std::greater<int>() 就行:
#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<>()(带空尖括号),这样可以利用C++14的透明运算符,自动推导类型。少写点模板参数,代码看着清爽。
3.3 逻辑运算函数对象
逻辑运算用得少一些,但在某些场景下很巧妙:
| 函数对象 | 运算 | 等价表达式 |
|---|---|---|
std::logical_and<T> |
逻辑与 | a && b |
std::logical_or<T> |
逻辑或 | a || b |
std::logical_not<T> |
逻辑非 | !a |
比如你想判断一个容器里是不是所有元素都非零:
#include <algorithm>
#include <vector>
#include <functional>
std::vector<int> data = {1, 2, 3, 0, 5};
bool all_nonzero = std::all_of(data.begin(), data.end(),
std::logical_not<bool>());
// 等等,这里逻辑不对。应该用 std::logical_not 对每个元素取反?
// 其实更简单:直接判断元素本身是否为 true
嗯,这个例子有点绕。说白了,逻辑函数对象更适合用在组合操作里,比如配合 std::transform 做批量判断。
3.4 为什么需要函数对象?
你可能会问:lambda表达式都出来了,谁还用这些老古董?
我承认,大部分场景下lambda更灵活。但函数对象有几个不可替代的优势:
- 类型固定:每个函数对象都是一个具体的类型,不像lambda每次生成一个独特的匿名类型。这在模板元编程里很重要。
- 可组合:标准库提供了
std::bind、std::not_fn等工具,可以把函数对象组合成更复杂的操作。 - 零开销:编译器可以轻松内联这些简单的函数对象,性能上和手写循环没区别。
- 语义清晰:看到
std::greater<int>(),你立刻知道这是降序。lambda虽然也能写,但多了一层心智负担。
3.5 实战:用函数对象做统计
我记得有一次,需要统计一批数据里大于某个阈值的元素个数。用函数对象配合 std::count_if,一行搞定:
#include <algorithm>
#include <vector>
#include <functional>
std::vector<double> scores = {85.5, 92.0, 67.3, 78.9, 95.1};
double threshold = 80.0;
// 统计大于80分的个数
int count = std::count_if(scores.begin(), scores.end(),
std::bind(std::greater<double>(),
std::placeholders::_1,
threshold));
std::cout << "超过80分的人数: " << count << std::endl;
这里用了 std::bind 把 std::greater 和阈值绑定在一起。当然,C++11之后我更推荐用lambda:
int count = std::count_if(scores.begin(), scores.end(),
[threshold](double s) { return s > threshold; });
你看,两种写法都能工作。选择哪种,取决于你的团队规范和代码上下文。
3.6 避坑指南
std::greater 做排序时,忘了传模板参数。结果 std::sort(vec.begin(), vec.end(), std::greater()) 编译报错。因为 std::greater 本身是一个模板类,必须实例化才能用。要么写 std::greater<int>(),要么用C++14的 std::greater<>()。
另一个要注意的是:函数对象默认按值传递。如果你在函数对象里存储了大数据(比如一个巨大的vector),记得用 std::ref 包装成引用,避免拷贝开销。
3.7 知识体系总览
下面这张图,把标准库函数对象的分类和关系梳理清楚了:
3.8 总结
标准库函数对象,说白了就是一组"预制"的小工具。它们不炫酷,但很实用。我建议你记住以下几点:
- 算术、比较、逻辑三大类,覆盖了日常90%的需求
- 配合STL算法使用,能写出简洁且高效的代码
- 类型固定,适合模板元编程和函数组合
- C++14的透明运算符(空尖括号)让使用更方便
下次写排序或者统计的时候,不妨试试直接用 std::greater<>() 或者 std::plus<>()。你会发现,代码读起来就像在说人话一样自然。