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或仿函数会更好。
四、三者的关系与选择
为了让你更直观地理解这三者的关系,我画了一张图:
总结一下我的选择原则:
- 写临时逻辑:用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