42. STL函数对象:仿函数、lambda表达式、std::function

函数对象这个话题,说实话,刚学C++的时候我总觉得有点绕。明明有函数指针,为什么还要搞出这么多花样?直到我在一个大型项目里被回调函数的维护折磨得够呛,才真正体会到这些东西的妙处。

今天咱们就把仿函数、lambda和std::function这三个东西彻底讲明白。它们本质上都是在解决同一个问题:如何把"行为"当作参数传递

一、仿函数(Functor)——最朴素的函数对象

仿函数,说白了就是一个重载了operator()的类。它看起来像函数,用起来也像函数,但它本质上是个对象。

核心思想:让对象可以像函数一样被调用。

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

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

你可能会问:直接用函数不就行了?嗯,这里有个关键区别——仿函数可以携带状态

我在项目中遇到过这样一个场景:需要统计容器中大于某个阈值的元素个数。如果用普通函数,阈值只能写死;但用仿函数,阈值可以灵活设置。

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

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

我的习惯:当需要传递"带配置的函数"时,优先考虑仿函数。比如排序时指定不同的比较规则,或者过滤时指定不同的条件。

二、lambda表达式——更简洁的匿名函数对象

lambda是C++11引入的语法糖。它本质上就是一个匿名的仿函数,但写起来简洁得多。

// 基本语法:[捕获列表](参数列表) -> 返回类型 { 函数体 }
auto add = [](int a, int b) -> int { return a + b; };
int result = add(3, 4);  // result == 7

lambda最强大的地方在于它的捕获机制。你可以把外部变量"抓"进来用。

int threshold = 5;
std::vector<int> data = {1, 5, 8, 3, 10, 2};

// 按值捕获 threshold
int count1 = std::count_if(data.begin(), data.end(), 
    [threshold](int value) { return value > threshold; });

// 按引用捕获 threshold(可以修改外部变量)
int count2 = std::count_if(data.begin(), data.end(), 
    [&threshold](int value) { return value > threshold; });

我曾经踩过的坑:按引用捕获局部变量时,如果lambda的生命周期超过了变量的生命周期,就会产生悬空引用。比如把lambda存起来,等变量销毁后再调用——程序直接崩溃。

捕获方式有几种,我整理了一张表:

捕获方式 语法 说明
按值捕获 [x] 捕获x的副本,lambda内部不能修改(除非加mutable)
按引用捕获 [&x] 捕获x的引用,可以修改外部变量
隐式按值捕获 [=] 捕获所有用到的外部变量的副本
隐式按引用捕获 [&] 捕获所有用到的外部变量的引用
混合捕获 [=, &x] 大部分按值,x按引用

我建议:尽量显式捕获,少用[=][&]。显式捕获让代码的意图更清晰,也更容易发现潜在问题。

三、std::function——万能函数包装器

std::function是C++11引入的一个通用多态函数包装器。它可以存储任何可调用对象:普通函数、仿函数、lambda、甚至成员函数指针。

#include <functional>

// 定义一个可以存储"接受两个int,返回int"的可调用对象
std::function<int(int, int)> func;

// 存储普通函数
int add(int a, int b) { return a + b; }
func = add;
int r1 = func(3, 4);  // r1 == 7

// 存储lambda
func = [](int a, int b) { return a * b; };
int r2 = func(3, 4);  // r2 == 12

// 存储仿函数
struct Multiply {
    int operator()(int a, int b) const { return a * b; }
};
func = Multiply();
int r3 = func(3, 4);  // r3 == 12

std::function最大的价值在于:它让不同类型的可调用对象有了统一的类型。这在设计回调系统、策略模式、事件驱动架构时特别有用。

我在项目中用std::function实现过一个简单的命令模式:

class Command {
private:
    std::function<void()> action_;
public:
    explicit Command(std::function<void()> action) : action_(std::move(action)) {}
    
    void execute() {
        if (action_) {
            action_();
        }
    }
};

// 使用
Command cmd1([]() { std::cout << "打开文件" << std::endl; });
Command cmd2([]() { std::cout << "保存数据" << std::endl; });

cmd1.execute();  // 输出:打开文件
cmd2.execute();  // 输出:保存数据

注意:std::function有性能开销。它内部使用了类型擦除,会带来一次间接调用。在性能敏感的代码中(比如每秒调用百万次),直接用lambda或仿函数会更好。

四、三者的关系与选择

为了让你更直观地理解这三者的关系,我画了一张图:

C++ 可调用对象体系 仿函数 (Functor) 重载 operator() 的类 可携带状态 性能最优 Lambda 表达式 匿名仿函数语法糖 简洁灵活 捕获外部变量 std::function 万能函数包装器 统一类型接口 有性能开销 语法简化 类型擦除 选择建议 • 需要携带状态且性能敏感 → 仿函数 • 需要简洁的临时逻辑 → lambda 表达式 • 需要统一类型存储/传递可调用对象 → std::function

总结一下我的选择原则:

  • 写临时逻辑:用lambda。比如给std::sort传比较函数,给std::for_each传处理逻辑。
  • 需要复用且带状态:用仿函数。比如自定义的比较器、过滤器。
  • 需要存储或传递可调用对象:用std::function。比如回调注册、策略模式。

核心要点:lambda是仿函数的语法糖,std::function是lambda/仿函数的类型擦除包装器。三者层层递进,各有适用场景。

五、实战:一个完整的例子

最后,我写一个综合例子,展示这三者如何协同工作:

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

// 仿函数:带状态的比较器
struct Comparator {
    bool ascending;
    explicit Comparator(bool asc) : ascending(asc) {}
    
    bool operator()(int a, int b) const {
        return ascending ? a < b : a > b;
    }
};

// 排序函数,接受std::function作为排序规则
void sortAndPrint(std::vector<int>& data, 
                  std::function<bool(int, int)> comp) {
    std::sort(data.begin(), data.end(), comp);
    for (int v : data) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
    
    // 使用仿函数
    std::cout << "升序: ";
    sortAndPrint(nums, Comparator(true));
    
    std::cout << "降序: ";
    sortAndPrint(nums, Comparator(false));
    
    // 使用lambda
    std::cout << "按绝对值升序: ";
    sortAndPrint(nums, [](int a, int b) {
        return std::abs(a) < std::abs(b);
    });
    
    return 0;
}

你看,同一个sortAndPrint函数,通过std::function这个"万能插座",可以接受仿函数、lambda、普通函数……想怎么玩就怎么玩。这就是函数对象的魅力所在。

我的经验:在实际项目中,80%的场景用lambda就够了。剩下15%用仿函数,只有5%才需要std::function。别一上来就用std::function,它虽然方便,但也不是免费的。


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