3. 标准库中的函数对象:std::plus, std::minus, std::greater, std::less 等。

说实话,刚学C++那会儿,我对这些标准库里的函数对象是有点不屑的。不就是个加法吗?我写个 a + b 不就完了,干嘛还要搞个 std::plus<int>()(a, b) 这么啰嗦?

后来我才明白,是我太年轻了。这些不起眼的小工具,其实是C++泛型编程的基石之一。它们让算法变得极其灵活,就像乐高积木里的标准接口——你不需要关心里面是什么,只要知道它能干什么。

核心概念: 函数对象(Functor)就是重载了 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::bindstd::not_fn 等工具,可以把函数对象组合成更复杂的操作。
  • 零开销:编译器可以轻松内联这些简单的函数对象,性能上和手写循环没区别。
  • 语义清晰:看到 std::greater<int>(),你立刻知道这是降序。lambda虽然也能写,但多了一层心智负担。
我的建议: 如果只是简单的算术或比较操作,优先用标准库函数对象。代码更短,意图更明确。只有当逻辑比较复杂(比如需要捕获外部变量)时,才考虑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::bindstd::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 知识体系总览

下面这张图,把标准库函数对象的分类和关系梳理清楚了:

标准库函数对象分类 std::functional 函数对象 算术运算 plus minus multiplies 比较运算 greater less equal_to 逻辑运算 logical_and logical_not logical_or 所有函数对象都定义在 <functional> 头文件中 每个函数对象都是一个类,重载了 operator() 典型使用场景 std::sort / std::count_if / std::transform / std::accumulate

3.8 总结

标准库函数对象,说白了就是一组"预制"的小工具。它们不炫酷,但很实用。我建议你记住以下几点:

  • 算术、比较、逻辑三大类,覆盖了日常90%的需求
  • 配合STL算法使用,能写出简洁且高效的代码
  • 类型固定,适合模板元编程和函数组合
  • C++14的透明运算符(空尖括号)让使用更方便

下次写排序或者统计的时候,不妨试试直接用 std::greater<>() 或者 std::plus<>()。你会发现,代码读起来就像在说人话一样自然。