第27章:函数对象与适配器——让C++更灵活

说实话,函数对象这个概念,我刚开始学C++时觉得挺鸡肋的。不就是个重载了operator()的类吗?用得着单独拿出来讲?直到我在一个大型交易系统中,需要把不同的排序策略动态传递给一个容器……嗯,那时候我才真正体会到函数对象的威力。

27.1 函数对象(仿函数)

函数对象,也叫仿函数。说白了,就是一个类,但它可以像函数一样被调用。

class Adder {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

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

你可能会问:直接用函数不就行了?为什么非要搞个类?

原因有三:

  • 可以携带状态——函数对象内部可以保存成员变量
  • 可以被内联——编译器更容易优化
  • 可以作为模板参数——这是普通函数指针做不到的

核心要点:函数对象本质上是一个类实例,但它通过operator()获得了“可调用”的能力。这比函数指针更灵活,比lambda更早出现。

27.2 std::function——万能函数包装器

我在项目中遇到过这样一个场景:需要把回调函数存到一个容器里,但回调函数的类型各不相同。有的返回void,有的返回int,参数个数也不一样。这时候std::function就派上用场了。

#include <functional>
#include <iostream>

void hello() {
    std::cout << "Hello, World!" << std::endl;
}

int add(int a, int b) {
    return a + b;
}

int main() {
    // 存储普通函数
    std::function<void()> f1 = hello;
    f1();  // 输出 Hello, World!

    // 存储lambda
    std::function<int(int, int)> f2 = [](int x, int y) { return x * y; };
    std::cout << f2(3, 4);  // 输出 12

    // 存储函数对象
    std::function<int(int, int)> f3 = add;
    std::cout << f3(10, 20);  // 输出 30

    return 0;
}

个人建议:能用auto的地方尽量用auto,别滥用std::function。它内部有类型擦除的开销,性能敏感的场景下要小心。我见过有人把std::function用在每秒调用百万次的热路径上,结果性能直接腰斩。

27.3 std::bind——参数绑定利器

std::bind可以“预绑定”函数的某些参数,生成一个新的可调用对象。说白了,就是给函数“打个折”,固定住一部分参数。

#include <functional>
#include <iostream>

void print(int a, int b, int c) {
    std::cout << a << ", " << b << ", " << c << std::endl;
}

int main() {
    // 绑定所有参数
    auto f1 = std::bind(print, 1, 2, 3);
    f1();  // 输出 1, 2, 3

    // 使用占位符
    auto f2 = std::bind(print, std::placeholders::_1, 100, std::placeholders::_2);
    f2(50, 200);  // 输出 50, 100, 200

    // 改变参数顺序
    auto f3 = std::bind(print, std::placeholders::_3, std::placeholders::_1, std::placeholders::_2);
    f3(10, 20, 30);  // 输出 30, 10, 20

    return 0;
}

我曾经在重构一个旧项目时,遇到一个函数有6个参数,但调用方每次都只传同样的前3个参数。用std::bind一包装,代码瞬间清爽了。

注意:C++17之后,lambda表达式基本可以替代std::bind。我个人更推荐用lambda,可读性更好。但如果你在维护老代码,还是得看懂std::bind

27.4 适配器:stack、queue、priority_queue

适配器,说白了就是“套壳”。它不自己管理数据,而是把底层容器包装一下,只暴露特定的接口。

27.4.1 stack——后进先出

#include <stack>
#include <iostream>

int main() {
    std::stack<int> s;
    s.push(1);
    s.push(2);
    s.push(3);

    while (!s.empty()) {
        std::cout << s.top() << " ";  // 输出 3 2 1
        s.pop();
    }
    return 0;
}

默认底层容器是deque,你也可以换成vectorlist

std::stack<int, std::vector<int>> s1;
std::stack<int, std::list<int>> s2;

27.4.2 queue——先进先出

#include <queue>

int main() {
    std::queue<int> q;
    q.push(1);
    q.push(2);
    q.push(3);

    while (!q.empty()) {
        std::cout << q.front() << " ";  // 输出 1 2 3
        q.pop();
    }
    return 0;
}

默认底层容器也是deque。为什么?因为deque在两端插入删除都是O(1),完美匹配queue的需求。

27.4.3 priority_queue——优先级队列

这个有点意思。它内部是一个堆结构,每次top()返回的都是最大(或最小)的元素。

#include <queue>
#include <vector>
#include <iostream>

int main() {
    // 默认是大顶堆
    std::priority_queue<int> pq;
    pq.push(3);
    pq.push(1);
    pq.push(4);
    pq.push(1);
    pq.push(5);

    while (!pq.empty()) {
        std::cout << pq.top() << " ";  // 输出 5 4 3 1 1
        pq.pop();
    }
    return 0;
}

想要小顶堆?用std::greater

std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;

避坑指南:我曾经在项目中用priority_queue做任务调度,结果发现自定义类型的比较器写错了,导致高优先级任务一直被压在最底下。调试了一下午才发现是operator<的方向搞反了。记住:默认是std::less,也就是大顶堆。

27.5 知识体系总览

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

第27章:函数对象与适配器 函数对象(仿函数) 重载 operator() 可携带状态 可内联优化 std::function 万能函数包装器 存储函数/lambda/仿函数 注意类型擦除开销 std::bind 参数预绑定 占位符 _1, _2, ... C++17后推荐lambda 容器适配器 stack(LIFO) queue(FIFO) priority_queue(堆)

27.6 综合示例:用适配器实现任务调度

最后,我把今天讲的东西串起来,写一个简单的任务调度器:

#include <iostream>
#include <queue>
#include <functional>
#include <string>

struct Task {
    int priority;
    std::string name;
    std::function<void()> action;

    bool operator<(const Task& other) const {
        return priority < other.priority;  // 优先级高的先执行
    }
};

int main() {
    std::priority_queue<Task> scheduler;

    scheduler.push({1, "低优先级任务", []() {
        std::cout << "执行低优先级任务" << std::endl;
    }});

    scheduler.push({10, "高优先级任务", []() {
        std::cout << "执行高优先级任务" << std::endl;
    }});

    scheduler.push({5, "中优先级任务", []() {
        std::cout << "执行中优先级任务" << std::endl;
    }});

    while (!scheduler.empty()) {
        auto task = scheduler.top();
        std::cout << "正在执行: " << task.name << std::endl;
        task.action();
        scheduler.pop();
    }

    return 0;
}

输出结果:

正在执行: 高优先级任务
执行高优先级任务
正在执行: 中优先级任务
执行中优先级任务
正在执行: 低优先级任务
执行低优先级任务

你看,priority_queue配合std::function,一个简单的任务调度器就出来了。这就是函数对象和适配器组合的威力。

最后说一句:函数对象和适配器,说白了就是C++给你提供的“乐高积木”。单个看都不复杂,但组合起来能解决很多实际问题。多写、多用,慢慢就熟练了。


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