18. std::function 类型擦除:统一存储可调用对象,运行时多态
好,咱们今天聊一个非常实用的东西——std::function。
说实话,我刚开始用C++那会儿,对这东西有点抵触。总觉得“不就是个函数指针的升级版吗?有必要搞这么复杂?”后来在项目中吃了不少亏,才明白它真正的价值在哪。
std::function 的核心能力,说白了就四个字:类型擦除。
什么意思?就是你不用管一个可调用对象到底是什么类型——是函数指针、lambda、还是函数对象——你只需要知道它“能调用”,并且有统一的签名就行。剩下的脏活累活,std::function 帮你扛了。
18.1 什么是类型擦除?
类型擦除,就是把具体类型的信息隐藏起来,只暴露一个统一的接口。
你想想看,lambda 表达式每个都是不同的类型。哪怕两个 lambda 长得一模一样,编译器也会给它们生成不同的类型。这就麻烦了——你想把它们存到一个容器里?不行。你想把它们作为参数传给一个函数?得用模板。你想在运行时动态决定调用哪个?更没门。
std::function 就是来解决这个问题的。它把各种可调用对象“包”起来,抹掉它们的具体类型,只保留一个统一的调用接口。
18.2 基本用法
先看个最简单的例子:
#include <functional>
#include <iostream>
void hello() {
std::cout << "Hello from function!\n";
}
int main() {
// 存储一个普通函数
std::function<void()> f1 = hello;
f1(); // 输出: Hello from function!
// 存储一个 lambda
std::function<int(int, int)> f2 = [](int a, int b) {
return a + b;
};
std::cout << f2(3, 4) << '\n'; // 输出: 7
// 存储一个函数对象
struct Adder {
int operator()(int a, int b) const {
return a + b;
}
};
std::function<int(int, int)> f3 = Adder{};
std::cout << f3(10, 20) << '\n'; // 输出: 30
return 0;
}
看到了吗?f1、f2、f3 的类型都是 std::function<void()> 或 std::function<int(int, int)>。不管背后是什么,调用方式完全一样。
18.3 类型擦除的内部机制
嗯,这里要稍微深入一点。我当年面试的时候被问过这个问题,答得不太好,后来专门去翻了源码。
std::function 内部大致是这样工作的:
- 它有一个 小对象优化(Small Object Optimization, SOO)的缓冲区。如果可调用对象比较小(比如一个无捕获的 lambda),就直接存在栈上,避免堆分配。
- 如果对象太大,就放到堆上。
- 它内部维护一个 虚函数表 风格的指针,指向一组操作:拷贝、移动、销毁、调用。
- 当你调用
f(args...)时,它通过这个虚函数表找到真正的调用函数,把参数转发过去。
说白了,std::function 自己就是一个函数对象,它内部用虚函数实现了运行时多态。
std::function 的开销非常小。我一般优先用这种形式。
18.4 什么时候用 std::function?
我个人习惯,在以下几种场景会毫不犹豫地用 std::function:
- 回调注册:比如事件系统、观察者模式。你需要存一堆回调,运行时决定调用哪个。
- 策略模式:运行时切换算法。比如排序时传入不同的比较器。
- 延迟执行:把任务存起来,稍后执行。
- 跨 API 边界:库函数需要接受用户自定义的可调用对象,但又不想用模板(模板会增加编译时间、暴露实现)。
举个例子,我在一个网络库中用过:
class EventLoop {
public:
using Task = std::function<void()>;
void postTask(Task task) {
tasks_.push_back(std::move(task));
}
void run() {
for (auto& task : tasks_) {
task(); // 统一调用
}
}
private:
std::vector<Task> tasks_;
};
// 使用
EventLoop loop;
loop.postTask([]() { std::cout << "Task 1\n"; });
loop.postTask([]{ std::cout << "Task 2\n"; });
loop.run();
你看,Task 的类型是统一的,但实际执行的任务可以完全不同。这就是类型擦除的威力。
18.5 性能考量
别以为 std::function 是万能的。它是有代价的。
| 对比项 | std::function | 模板 + 函数指针 | 直接调用 lambda |
|---|---|---|---|
| 调用开销 | 虚函数调用(间接) | 直接调用 | 直接调用(可内联) |
| 内存开销 | 至少 32-64 字节(含 SOO) | 8 字节(函数指针) | 取决于捕获 |
| 类型灵活性 | 极高 | 低 | 低 |
| 编译时间 | 低 | 高(模板实例化) | 低 |
std::function。我曾经在一个高频交易系统中发现,把 std::function 换成模板 + 函数指针,性能提升了 30%。虚函数调用 + 无法内联,在热点路径上是很痛的。
18.6 避坑指南
我曾经踩过一个坑:把 lambda 捕获了 this 指针,然后存到 std::function 里,结果对象销毁后调用这个 function,直接崩溃。
class Widget {
public:
void registerCallback() {
// 危险!捕获了 this,但 this 可能被销毁
callback_ = [this]() {
doSomething(); // 如果 Widget 已销毁,这里就 UB 了
};
}
void doSomething() { /* ... */ }
private:
std::function<void()> callback_;
};
正确的做法是:要么确保生命周期,要么用 std::weak_ptr 或传值捕获。
// 安全做法:用 shared_from_this()
class Widget : public std::enable_shared_from_this<Widget> {
public:
void registerCallback() {
auto self = shared_from_this();
callback_ = [self]() {
self->doSomething(); // 安全
};
}
};
18.7 与 std::bind 的关系
很多人喜欢用 std::bind 配合 std::function。我个人不太推荐 std::bind,因为 lambda 更清晰、更现代。
// 不推荐
std::function<int(int)> f = std::bind(std::plus<int>(), 10, std::placeholders::_1);
// 推荐
std::function<int(int)> f = [](int x) { return x + 10; };
lambda 的可读性明显更好。而且 lambda 的捕获机制更灵活,不容易出错。
18.8 知识体系图
下面这张图帮你理清 std::function 在整个可调用对象生态中的位置:
18.9 总结
std::function 是 C++ 中实现类型擦除的利器。它让你可以统一存储和调用各种可调用对象,在运行时实现多态。
但记住:它是有代价的。虚函数调用、可能的堆分配、无法内联——这些在性能敏感的场景下要慎重。
我个人建议:
- 在回调、事件、策略等场景,放心用
std::function。 - 在热点路径、高频调用处,考虑用模板或函数指针替代。
- 永远注意生命周期问题,别让
std::function持有悬空指针。
好了,这一章就到这里。记住一句话:类型擦除让你在运行时忘记类型,但别忘了性能和安全。
公众号:蓝海资料掘金营,微信deep3321