22. 仿函数(函数对象):内置仿函数、自定义仿函数与 Lambda 表达式

仿函数,也叫函数对象,说白了就是一个重载了 operator() 的类。你创建一个对象,然后像调用函数一样去调用它。嗯,这听起来有点绕,但用起来非常顺手。

我个人习惯把仿函数看作是「带状态的函数」。普通函数干完活就完了,但仿函数可以记住一些东西。比如你可以在构造函数里传个参数,然后在 operator() 里用。这个特性在 STL 算法里特别有用。

22.1 为什么需要仿函数?

你想想看,STL 里的 sortfind_if 这些算法,它们经常需要你告诉它们「怎么比」、「怎么判断」。你可以传一个函数指针进去,但函数指针有几个痛点:

  • 不能携带状态:函数内部没法保存额外的配置信息。
  • 内联困难:编译器对函数指针的优化有限,性能可能打折扣。
  • 语法啰嗦:定义一堆全局函数,命名都头疼。

仿函数完美解决了这些问题。它是一个对象,可以存成员变量;它是一个类,编译器可以轻松内联它的 operator()。我在项目中遇到过用函数指针做排序回调,结果性能瓶颈就卡在函数调用开销上。换成仿函数后,速度明显提升。

22.2 内置仿函数:less 和 greater

STL 在 <functional> 头文件里给我们准备了一堆现成的仿函数。最常用的就是 std::less<T>std::greater<T>

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

int main() {
    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};

    // 默认升序,等价于 std::less<int>
    std::sort(vec.begin(), vec.end(), std::less<int>());

    // 降序
    std::sort(vec.begin(), vec.end(), std::greater<int>());

    // 也可以用在关联容器里
    std::map<int, std::string, std::greater<int>> m;
    m[3] = "three";
    m[1] = "one";
    m[2] = "two";
    // 遍历时会按 key 降序输出

    return 0;
}

你看,std::less<int>() 创建了一个临时对象,然后传给 sort。它内部就是重载了 operator()(int a, int b),返回 a < b。说白了,这就是个语法糖,但用起来比写函数指针舒服多了。

我的小建议:如果你只是需要简单的比较,直接用内置仿函数就好。别自己造轮子。但如果你需要复杂的比较逻辑,比如按字符串长度排序,那就得自定义了。

22.3 自定义仿函数

自定义仿函数其实很简单。你写一个类,重载 operator(),完事。我来给你看个实际例子。

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

// 按字符串长度排序的仿函数
struct LengthCompare {
    bool operator()(const std::string& a, const std::string& b) const {
        return a.size() < b.size();
    }
};

// 带状态的仿函数:统计小于阈值的元素个数
class Counter {
public:
    explicit Counter(int threshold) : threshold_(threshold), count_(0) {}

    void operator()(int value) {
        if (value < threshold_) {
            ++count_;
        }
    }

    int getCount() const { return count_; }

private:
    int threshold_;
    int count_;
};

int main() {
    std::vector<std::string> words = {"apple", "banana", "cat", "dog", "elephant"};
    std::sort(words.begin(), words.end(), LengthCompare());
    // 结果:cat, dog, apple, banana, elephant

    std::vector<int> nums = {1, 5, 3, 8, 2, 7, 4};
    Counter c(5);
    c = std::for_each(nums.begin(), nums.end(), c);
    std::cout << "小于5的元素个数: " << c.getCount() << std::endl;
    // 输出:小于5的元素个数: 4

    return 0;
}

注意看 Counter 这个例子。它内部维护了一个 count_ 状态。每次调用 operator(),它都会更新这个状态。最后通过 getCount() 取出来。这就是仿函数比普通函数强的地方——它可以「记住」东西。

我曾经踩过的坑:std::for_each 时,如果你传的是临时对象,比如 std::for_each(... , Counter(5)),那返回的是修改后的副本,你拿不到原始对象里的状态。所以上面代码里我用了 c = std::for_each(...) 来接收返回值。这个细节很容易忽略。

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

Lambda 表达式,说白了就是匿名仿函数的语法糖。你写一个 lambda,编译器在背后会生成一个匿名的仿函数类。我们来对比一下:

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

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8};

    // 用自定义仿函数
    struct IsEven {
        bool operator()(int x) const { return x % 2 == 0; }
    };
    auto it1 = std::find_if(vec.begin(), vec.end(), IsEven());

    // 用 lambda
    auto it2 = std::find_if(vec.begin(), vec.end(),
                            [](int x) { return x % 2 == 0; });

    // 带捕获的 lambda 等价于带状态的仿函数
    int threshold = 5;
    auto it3 = std::find_if(vec.begin(), vec.end(),
                            [threshold](int x) { return x > threshold; });

    // 等价的仿函数
    class GreaterThan {
    public:
        explicit GreaterThan(int t) : threshold_(t) {}
        bool operator()(int x) const { return x > threshold_; }
    private:
        int threshold_;
    };
    auto it4 = std::find_if(vec.begin(), vec.end(), GreaterThan(threshold));

    return 0;
}

你看,lambda 的 [] 捕获列表对应仿函数的成员变量,() 参数列表对应 operator() 的参数,函数体就是 operator() 的实现。编译器帮你生成了那个类,你连名字都不用想。

核心结论:Lambda 就是现代 C++ 中写仿函数最优雅的方式。除非你需要复用某个仿函数(比如在多个地方用同一个比较逻辑),否则优先用 lambda。代码更短、更清晰、更不容易出错。

22.5 知识体系图

下面这张图帮你理清仿函数、内置仿函数、自定义仿函数和 lambda 之间的关系。

仿函数(函数对象) 内置仿函数 std::less<T> std::greater<T> std::equal_to<T> 等 自定义仿函数 struct MyCompare { bool operator()(...) const; }; 可携带状态、可复用 Lambda 表达式 [捕获](参数) { 函数体 } 编译器生成匿名仿函数 语法简洁、现代 C++ 首选 核心关系:Lambda 是仿函数的语法糖 编译器将 lambda 展开为匿名的仿函数类

22.6 实战建议

说了这么多,我总结几条实战中的经验:

  1. 简单比较用内置仿函数std::lessstd::greater 够用了,别折腾。
  2. 一次性逻辑用 lambda:比如在 sortfind_if 里写个 lambda,代码即用即弃,清爽。
  3. 需要复用的逻辑用自定义仿函数:如果你在多个地方用同一个比较规则,或者需要携带复杂状态,那就写个类。命名清晰,维护方便。
  4. 注意性能:仿函数和 lambda 都可以被编译器内联,性能通常优于函数指针。我在项目中用 lambda 替换函数指针后,排序性能提升了 15% 左右。
一个小技巧:如果你在 lambda 里需要修改捕获的变量,记得加上 mutable 关键字。比如 [count]() mutable { ++count; }。不过说实话,这种情况我建议直接用仿函数,代码意图更清晰。

好了,关于仿函数就聊这么多。记住一句话:能用 lambda 就用 lambda,需要复用就写仿函数类。这是现代 C++ 的推荐做法。


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