17. 函数对象(仿函数):让函数“对象化”

函数对象,也叫仿函数(Functor),是 C++ 里一个很有意思的设计。说白了,就是让一个类的实例可以像函数一样被调用。你可能会问:“我有普通函数,为什么还要搞个对象出来?” 嗯,这个问题我当年也困惑过。

我个人习惯把仿函数理解为“带状态的函数”。普通函数是“死”的,你传什么参数,它返回什么结果,中间没有记忆。但仿函数不一样——它是个对象,可以有自己的成员变量,可以保存状态。这一点在实际工程中非常有用。

核心概念: 重载了 operator() 的类,其实例就是函数对象。

17.1 函数对象的基本概念

先看一个最简单的例子:

#include <iostream>

class Greeter {
public:
    void operator()(const std::string& name) const {
        std::cout << "Hello, " << name << "!" << std::endl;
    }
};

int main() {
    Greeter greet;
    greet("Alice");  // 像函数一样调用
    greet("Bob");
    return 0;
}

你看,greet("Alice") 这个写法,和普通函数调用一模一样。但 greet 其实是个对象。这就是仿函数最直观的样子。

为什么叫“仿函数”?因为它“模仿”了函数的行为。在 STL 里,仿函数大量用于算法中,比如 std::sort 的第三个参数。

17.2 预定义的函数对象

STL 已经帮我们准备了一些常用的仿函数,放在 <functional> 头文件里。我个人最常用的是 std::lessstd::greater

仿函数 作用 示例
std::less<T> 判断 a < b less<int>()(3, 5) 返回 true
std::greater<T> 判断 a > b greater<int>()(3, 5) 返回 false
std::equal_to<T> 判断 a == b equal_to<int>()(3, 3) 返回 true
std::not_equal_to<T> 判断 a != b not_equal_to<int>()(3, 5) 返回 true
std::plus<T> 返回 a + b plus<int>()(3, 5) 返回 8
std::minus<T> 返回 a - b minus<int>()(3, 5) 返回 -2

这些预定义的仿函数,说白了就是帮你省去写简单比较逻辑的功夫。比如你想降序排序一个 vector

#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,你得自己写一个比较函数或者 lambda。有了它,一行搞定。

小技巧: 在 C++14 之后,你可以用 std::greater<>(空尖括号),它会自动推导类型,写起来更简洁。

17.3 自定义仿函数

预定义的仿函数毕竟有限。实际项目中,我们经常需要自定义。我在项目中遇到过这样一个场景:需要根据员工的“绩效分 + 工龄”综合排序。这时候自定义仿函数就派上用场了。

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

struct Employee {
    std::string name;
    int score;      // 绩效分
    int years;      // 工龄
};

struct EmployeeComparator {
    bool operator()(const Employee& a, const Employee& b) const {
        // 先按绩效分降序,相同则按工龄降序
        if (a.score != b.score)
            return a.score > b.score;
        return a.years > b.years;
    }
};

int main() {
    std::vector<Employee> team = {
        {"Alice", 85, 3},
        {"Bob",   92, 5},
        {"Carol", 85, 7}
    };

    std::sort(team.begin(), team.end(), EmployeeComparator());

    for (const auto& e : team) {
        std::cout << e.name << ": " << e.score << "分, " << e.years << "年" << std::endl;
    }
    return 0;
}

输出:

Bob: 92分, 5年
Carol: 85分, 7年
Alice: 85分, 3年

你看,EmployeeComparator 这个仿函数封装了完整的排序逻辑。如果以后排序规则变了,你只需要改这一个地方,不用到处翻代码。

注意: 自定义仿函数时,operator() 最好声明为 const。因为 STL 算法通常要求比较操作不修改对象状态。我曾经因为漏了 const,在编译时遇到一堆看不懂的模板错误……嗯,血的教训。

17.4 Lambda 表达式与仿函数的关系

Lambda 表达式是 C++11 引入的语法糖。说白了,它就是匿名仿函数。编译器会把 lambda 表达式展开成一个匿名的仿函数类。

看这个对比:

// 方式一:自定义仿函数
struct Multiply {
    int factor;
    Multiply(int f) : factor(f) {}
    int operator()(int x) const { return x * factor; }
};
std::vector<int> result1;
std::transform(vec.begin(), vec.end(), std::back_inserter(result1), Multiply(3));

// 方式二:Lambda 表达式
std::vector<int> result2;
std::transform(vec.begin(), vec.end(), std::back_inserter(result2),
               [factor = 3](int x) { return x * factor; });

两种写法效果完全一样。但 lambda 更简洁,尤其适合一次性使用的场景。那什么时候该用仿函数呢?我个人总结了几点:

  • 逻辑需要复用:如果同一个比较/操作要在多个地方使用,定义成仿函数更合适。
  • 逻辑比较复杂:lambda 写长了可读性会下降,仿函数可以拆成多个成员函数。
  • 需要继承或组合:仿函数是类,可以继承、可以组合,lambda 做不到。
  • 需要调试:仿函数有名字,调试器里看得清楚。lambda 在调试器里通常显示为 __lambda_xxx,很头疼。

我记得有一次排查线上 bug,就是因为 lambda 捕获了某个变量的引用,但那个变量在 lambda 执行前就被销毁了。如果用仿函数,这种问题会更容易发现——因为成员变量的生命周期是明确的。

17.5 知识体系总览

下面这张图帮你理清本章的核心脉络:

函数对象(仿函数)知识体系 仿函数 (Functor) 核心概念 重载 operator() 对象可像函数一样调用 可保存状态(成员变量) STL 预定义仿函数 less / greater / equal_to plus / minus / multiplies 位于 <functional> 头文件 常用于 sort / transform 等算法 自定义仿函数 灵活封装业务逻辑 可复用、可调试 适合复杂比较规则 operator() 建议加 const Lambda 表达式 = 匿名仿函数(语法糖)

17.6 避坑指南与最佳实践

最后,分享几个我这些年积累的经验:

  • 仿函数 vs 函数指针:仿函数可以内联,性能更好。函数指针有间接调用开销。在性能敏感的代码里,我优先用仿函数或 lambda。
  • 小心捕获引用:lambda 捕获 [&] 很方便,但容易造成悬空引用。我建议优先按值捕获,或者显式列出要捕获的变量。
  • 命名要清晰:自定义仿函数的名字最好能体现它的用途,比如 ByScoreDescending,而不是 MyComparator。代码是写给人看的。
  • 善用 std::function:如果你需要在运行时动态决定调用哪个仿函数,可以用 std::function 来包装。但注意它会有一些运行时开销。
一句话总结: 仿函数是“有状态、可复用”的函数;lambda 是“轻量、一次性”的仿函数。两者相辅相成,没有谁取代谁。

好了,关于函数对象的内容就讲到这里。希望你能在实际项目中找到它的用武之地。


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