8. 函数对象:从概念到实战

函数对象,也叫仿函数(Functor)。说白了,就是一个重载了 operator() 的类。你想想看,类实例化出来的对象,居然能像函数一样被调用——这就是函数对象的核心。

我刚开始学 STL 时,觉得这东西有点多余。明明有普通函数,为什么还要搞个类出来?直到我在项目中遇到一个场景:需要给 sort 算法传一个比较规则,而且这个规则还要带状态。嗯,普通函数做不到,函数对象却可以。

8.1 函数对象的基本概念

先看一个最简单的例子:

#include <iostream>

// 定义一个函数对象
class MyAdd {
public:
    int operator()(int a, int b) {
        return a + b;
    }
};

int main() {
    MyAdd add;          // 创建对象
    int result = add(3, 5);  // 像函数一样调用
    std::cout << result;    // 输出 8
    return 0;
}

看到没?add(3, 5) 这个写法,跟普通函数一模一样。但 add 其实是个对象。这就是函数对象最直观的样子。

8.2 为什么需要函数对象?

我个人习惯用函数对象,主要有三个理由:

  • 可以携带状态:普通函数只能靠全局变量或静态变量,函数对象可以在成员变量里存数据
  • 内联优化更好:编译器更容易内联 operator(),性能比函数指针好
  • STL 算法天然支持:很多算法都接受函数对象作为参数

举个例子,我想统计一个容器里大于某个阈值的元素个数。用普通函数你得写死阈值,用函数对象就可以灵活设置:

#include <iostream>
#include <vector>
#include <algorithm>

class GreaterThan {
private:
    int threshold_;
public:
    GreaterThan(int t) : threshold_(t) {}
    bool operator()(int value) {
        return value > threshold_;
    }
};

int main() {
    std::vector<int> data = {1, 5, 8, 3, 10, 2};
    int count = std::count_if(data.begin(), data.end(), GreaterThan(5));
    std::cout << "大于5的元素个数: " << count;  // 输出 2
    return 0;
}

我在项目中遇到过类似的需求:需要根据用户输入的阈值动态过滤数据。用函数对象,阈值作为构造参数传进去,非常优雅。

8.3 谓词:返回 bool 的函数对象

谓词(Predicate)是函数对象的一个特例——它的 operator() 返回 bool 值。STL 算法里大量使用谓词来做条件判断。

一元谓词

接受一个参数,返回 bool。比如判断一个数是否为偶数:

class IsEven {
public:
    bool operator()(int n) {
        return n % 2 == 0;
    }
};

// 使用
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = std::find_if(v.begin(), v.end(), IsEven());
// it 指向 2

二元谓词

接受两个参数,返回 bool。常用于排序和比较:

class Greater {
public:
    bool operator()(int a, int b) {
        return a > b;
    }
};

// 降序排序
std::vector<int> v = {3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), Greater());
// 结果: 5, 4, 3, 1, 1
小技巧:一元谓词常用于 find_if、count_if、remove_if 等算法;二元谓词常用于 sort、stable_sort、lower_bound 等算法。记住这个对应关系,用起来会顺手很多。

8.4 内建函数对象

STL 已经帮我们封装好了常用的函数对象,放在 <functional> 头文件里。我个人觉得,能用内建的就不用自己写,省事又不容易出错。

分类 内建函数对象 说明
算术类 plus<T> 加法
minus<T> 减法
multiplies<T> 乘法
divides<T> 除法
modulus<T> 取模
negate<T> 取反
关系类 equal_to<T> 等于
not_equal_to<T> 不等于
greater<T> 大于
less<T> 小于
greater_equal<T> 大于等于
less_equal<T> 小于等于
逻辑类 logical_and<T> 逻辑与
logical_or<T> 逻辑或

举个例子,用内建的 greater 实现降序排序:

#include <functional>
#include <vector>
#include <algorithm>

std::vector<int> v = {3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), std::greater<int>());
// 结果: 5, 4, 3, 1, 1

你看,一行代码搞定,比自己写 Greater 类简洁多了。

8.5 函数对象 vs 函数指针 vs Lambda

很多初学者会问:这三者到底怎么选?我根据经验总结一下:

  • 函数指针:C 风格,性能一般,不推荐在 C++ 新代码中使用
  • 函数对象:需要携带状态时首选,性能好,代码可读性强
  • Lambda 表达式:C++11 引入,临时用一下很方便,但复杂逻辑还是建议用函数对象
避坑指南:我曾经在项目里用 Lambda 写了一个很复杂的比较逻辑,结果代码维护时根本看不懂。后来改成具名的函数对象,加上注释,同事都说好。所以我的建议是:Lambda 适合简单场景,超过 5 行就考虑用函数对象。

8.6 知识体系总览

下面这张图帮你理清函数对象的整体脉络:

函数对象 重载 operator() 的类 一元 / 二元谓词 内建函数对象 可携带状态 内联优化好 STL 算法天然支持 一元谓词:一个参数 二元谓词:两个参数 用于 find_if / sort 等 算术类:plus, minus... 关系类:greater, less... 逻辑类:and, or... 核心:用对象模拟函数行为

核心要点回顾

  • 函数对象是重载了 operator() 的类,可以像函数一样调用
  • 谓词是返回 bool 的函数对象,分一元和二元两种
  • STL 内建了算术、关系、逻辑三大类函数对象,在 <functional> 中
  • 函数对象比函数指针更灵活,比 Lambda 更适合复杂逻辑

函数对象这东西,说白了就是 C++ 里「万物皆可对象」思想的体现。把行为封装成对象,你就能享受面向对象的一切好处:状态、继承、多态。我在实际项目中,经常用函数对象来做策略模式,把不同的算法逻辑封装成不同的函数对象,调用方只需要传入合适的对象即可。这种设计,既灵活又解耦。


公众号:蓝海资料掘金营,微信deep3321