选择 std::function、模板还是裸函数指针,核心在于权衡灵活性、性能和代码复杂度。以下是详细的决策指南:
快速决策图
c
是否需要存储回调?(类成员、容器、跨函数传递)
├── 是 → 是否需要类型擦除?(存储不同类型但相同签名的可调用对象)
│ ├── 是 → std::function(或 std::function_ref C++23)
│ └── 否 → 模板(如 std::vector<具体类型>)
└── 否 → 编译时确定类型?
├── 是 → 模板(零开销,最优性能)
└── 否 → 裸函数指针(C接口、极简场景)
详细场景对比
1. 模板(template<typename F>)--- 首选,如果可能
适用场景:
- 回调只在单个函数内使用,不需要存储
- 性能敏感(高频调用)
- 编译时就能确定可调用对象类型
cpp
// ✅ 推荐:零开销抽象
template<typename Func>
void execute(Func&& func) {
func(); // 直接内联,无间接调用开销
}
// 使用
execute([]{ std::cout << "lambda\n"; });
execute(my_functor);
execute(free_function);
优点:
- 零运行时开销(完全内联)
- 支持任意可调用对象(lambda、仿函数、函数指针)
- 类型安全,编译期检查
缺点:
- 不能存储(无法作为类成员或放入容器)
- 必须在头文件中实现(或显式实例化)
- 编译时间可能增加
2. std::function --- 需要类型擦除时
适用场景:
- 需要存储回调(类成员变量)
- 需要放入容器(
std::vector<std::function<...>>) - 需要运行时多态(不同类型,相同签名)
- 跨层传递(如从UI层传递到业务层)
cpp
// ✅ 推荐:需要存储的场景
class TaskScheduler {
std::vector<std::function<void()>> tasks_; // 存储不同类型任务
public:
void add(std::function<void()> task) { // 接受任何可调用对象
tasks_.push_back(std::move(task));
}
void run() {
for (auto& t : tasks_) t();
}
};
// 可以混合添加:
scheduler.add([]{ /* lambda */ });
scheduler.add(std::bind(&Class::method, &obj));
scheduler.add(free_function);
优点:
- 极强的灵活性,真正的类型擦除
- 可以存储、复制、移动
- 接口清晰,解耦调用方和实现方
缺点:
- 运行时开销(通常 1-2 次间接跳转)
- 可能堆分配(大 lambda/仿函数)
- 可能抛出
bad_function_call
性能优化:
cpp
// C++23 引入 std::function_ref:轻量级、非拥有、零分配
void process(std::function_ref<void(int)> callback); // 只读引用,不存储
3. 裸函数指针 --- C 接口或极简场景
适用场景:
- C 语言接口/兼容
- 系统级编程(信号处理、线程创建)
- 绝对零开销且确定无捕获 lambda
- 嵌入式/资源极度受限环境
cpp
// ✅ 推荐:C API 回调
void qsort(void* base, size_t n, size_t size,
int (*cmp)(const void*, const void*)); // C 标准库
// ✅ 推荐:信号处理
void (*signal(int sig, void (*func)(int)))(int);
// ❌ 不推荐:现代 C++ 应用开发
void register_callback(void (*cb)(int)); // 无法接受 lambda!
优点:
- 最小内存占用(通常 4/8 字节)
- 与 C 完全兼容
- 最简单的 ABI
缺点:
- 无法接受有捕获的 lambda
- 无法接受仿函数
- 类型不安全(
void*转换)
实战选择指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 算法中的自定义比较/操作 | 模板 | std::sort(begin, end, cmp) 式接口 |
| 事件/回调系统 | std::function |
需要存储不同监听者 |
| 依赖注入/策略模式 | 模板 或 std::function |
模板编译期确定,std::function 运行时切换 |
| 跨 DLL 边界 | std::function 或 裸指针 |
注意 ABI 兼容性 |
| C API 封装 | 裸指针 | 被迫使用 |
| 高频调用(>100万次/秒) | 模板 | 避免虚函数开销 |
| 延迟执行/任务队列 | std::function |
必须存储 |
| 配置驱动(运行时决定行为) | std::function |
运行时多态 |
现代 C++ 演进(C++23)
cpp
// C++23 std::function_ref:只读、非拥有、零分配
void process(std::function_ref<int(int)> op);
// 对比:
// std::function : 拥有,可存储,有分配开销
// std::function_ref : 不拥有,轻量,仅借用
// 模板 : 编译期确定,最优性能,无存储能力
总结建议
- 默认用模板:如果不需要存储,模板是零成本抽象的黄金标准
- 存储用
std::function:需要放入容器或类成员时 - C 接口用裸指针:被迫场景,或极端资源受限
- 关注 C++23 :
std::function_ref可能成为"需要引用但不存储"场景的最佳选择
核心原则 :先尝试模板,遇到存储需求再降级到 std::function,裸指针作为最后手段。