仿函数与适配器:让C++算法“活”起来

各位,今天我们来聊聊仿函数和适配器。说实话,这两个东西刚接触时有点绕,但用顺了之后,你会发现它们就像瑞士军刀——小巧、灵活、关键时刻能救命。

1. 仿函数:长得像函数,其实是个对象

仿函数(Functor),说白了就是重载了 operator() 的类。你想想看,一个对象能像函数一样被调用,是不是很神奇?

核心思想:用对象保存状态,用调用运算符执行逻辑。

// 一个简单的仿函数
struct Adder {
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    Adder add;
    int result = add(3, 4);  // 像函数一样调用
    // result == 7
    return 0;
}

我在项目中遇到过这样的场景:需要给 std::sort 传一个比较器,但比较逻辑依赖于某个运行时参数。普通函数做不到,lambda 当时还没普及,仿函数就成了救星。

我的习惯:如果逻辑简单且不需要状态,优先用 lambda;如果需要保存状态或复用,就用仿函数。

2. 函数适配器:给函数“穿衣服”

适配器(Adapter)的作用,就是改变一个函数的行为。C++ 标准库提供了几个经典的适配器:std::bindstd::not_fnstd::function

2.1 std::bind:绑定参数,生成新函数

std::bind 可以固定某些参数,生成一个参数更少的可调用对象。嗯,这里要注意:占位符 std::placeholders::_1 表示“这个参数留给调用时再传”。

#include <functional>

bool isGreater(int a, int b) {
    return a > b;
}

int main() {
    using namespace std::placeholders;
    // 绑定第二个参数为 10,生成一个一元谓词
    auto isGreaterThan10 = std::bind(isGreater, _1, 10);
    
    std::vector<int> data = {5, 12, 3, 18, 7};
    int count = std::count_if(data.begin(), data.end(), isGreaterThan10);
    // count == 2 (12 和 18)
    return 0;
}

我曾经踩过的坑:bind 绑定成员函数时,第一个参数必须是对象指针或智能指针。如果传了临时对象,生命周期一结束就悬空了。后来我改用 lambda 或 std::mem_fn 才彻底解决。

2.2 std::not_fn:逻辑取反

这个适配器简单粗暴——把谓词的结果取反。C++17 引入,替代了老旧的 std::not1std::not2

auto isEven = [](int x) { return x % 2 == 0; };
auto isOdd = std::not_fn(isEven);

std::vector<int> nums = {1,2,3,4,5};
int oddCount = std::count_if(nums.begin(), nums.end(), isOdd);
// oddCount == 3

2.3 std::function:万能函数包装器

std::function 可以存储任何可调用对象——函数指针、lambda、仿函数、bind 表达式。说白了,它就是一个类型擦除的容器。

#include <functional>

void hello() { std::cout << "Hello\n"; }

int main() {
    std::function<void()> func;
    
    func = hello;           // 函数指针
    func();                 // 输出 Hello
    
    func = []() { std::cout << "Lambda\n"; };
    func();                 // 输出 Lambda
    
    auto bound = std::bind(hello);
    func = bound;           // bind 表达式
    func();                 // 输出 Hello
    
    return 0;
}

性能提醒:std::function 有轻微开销(虚函数调用或小对象优化)。如果性能敏感,尽量用 auto 或模板。我一般在回调接口、事件系统里用它,日常算法中尽量不用。

3. 实战:用适配器改造排序

来看一个综合例子。假设你需要对一个 std::vector<std::string> 按字符串长度降序排序,但长度比较逻辑需要复用。

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

bool shorter(const std::string& a, const std::string& b) {
    return a.size() < b.size();
}

int main() {
    std::vector<std::string> words = {"apple", "banana", "cherry", "date"};
    
    // 用 not_fn 实现降序
    std::sort(words.begin(), words.end(), 
              std::not_fn(std::function<bool(const std::string&, const std::string&)>(shorter)));
    
    // 或者用 bind 绑定参数顺序
    using namespace std::placeholders;
    std::sort(words.begin(), words.end(), 
              std::bind(shorter, _2, _1));  // 交换参数实现降序
    
    // 输出: banana, cherry, apple, date
    return 0;
}

个人经验:实际项目中我很少用 bind 做参数交换,因为可读性差。更推荐 lambda:[](const auto& a, const auto& b) { return a.size() > b.size(); }。但 bind 在泛型代码中仍有价值,比如需要将成员函数适配成普通函数时。

4. 知识体系图

下面这张图帮你理清仿函数和适配器的关系:

可调用对象 仿函数 (Functor) 函数适配器 Lambda 表达式 重载 operator() 可保存状态 std::bind std::not_fn std::function C++11 引入 闭包捕获 三者可互相转换,按需选择

5. 避坑指南与最佳实践

  • bind 的占位符别搞混:_1 是调用时的第一个参数,不是 bind 时的第一个参数。我见过有人把 _1_2 写反,排查了半天。
  • std::function 的构造开销:每次赋值都会拷贝或移动可调用对象。如果对象很大(比如捕获了大量数据的 lambda),性能会下降。建议用 std::ref 包装引用。
  • not_fn 与老式适配器:C++17 之后请用 std::not_fn,不要再用 std::not1std::not2。后者需要定义 argument_type,麻烦且易错。
  • 仿函数 vs lambda:如果只需要一次性的简单逻辑,lambda 更简洁。如果需要多次复用且带状态,仿函数更清晰。

我的个人习惯:在头文件中定义仿函数,在源文件中用 lambda。这样接口清晰,实现灵活。

6. 总结

仿函数和适配器是 C++ 泛型编程的基石。仿函数让对象可调用,适配器让函数可组合。说白了,它们就是让代码更灵活、更可复用的工具。你想想看,没有它们,std::sort 怎么支持自定义排序?回调系统怎么实现?

嗯,今天就到这里。记住:多用 lambda,少用 bind,必要时用仿函数。这样写出来的代码,别人看得懂,自己也维护得了。

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