选择 std::function、模板还是裸函数指针,核心在于权衡灵活性、性能和代码复杂度。以下是详细的决策指南:


快速决策图

是否需要存储回调?(类成员、容器、跨函数传递)
├── 是 → 是否需要类型擦除?(存储不同类型但相同签名的可调用对象)
│   ├── 是 → std::function(或 std::function_ref C++23)
│   └── 否 → 模板(如 std::vector<具体类型>)
└── 否 → 编译时确定类型?
    ├── 是 → 模板(零开销,最优性能)
    └── 否 → 裸函数指针(C接口、极简场景)

详细场景对比

1. 模板(template<typename F>)— 首选,如果可能

适用场景:

  • 回调只在单个函数内使用,不需要存储
  • 性能敏感(高频调用)
  • 编译时就能确定可调用对象类型
//  推荐:零开销抽象
template<typename Func>
void execute(Func&& func) {
    func();  // 直接内联,无间接调用开销
}

// 使用
execute([]{ std::cout << "lambdan"; });
execute(my_functor);
execute(free_function);

优点:

  • 零运行时开销(完全内联)
  • 支持任意可调用对象(lambda、仿函数、函数指针)
  • 类型安全,编译期检查

缺点:

  • 不能存储(无法作为类成员或放入容器)
  • 必须在头文件中实现(或显式实例化)
  • 编译时间可能增加

2. std::function需要类型擦除时

适用场景:

  • 需要存储回调(类成员变量)
  • 需要放入容器(std::vector<std::function<...>>
  • 需要运行时多态(不同类型,相同签名)
  • 跨层传递(如从UI层传递到业务层)
//  推荐:需要存储的场景
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

性能优化:

// C++23 引入 std::function_ref:轻量级、非拥有、零分配
void process(std::function_ref<void(int)> callback);  // 只读引用,不存储

3. 裸函数指针 — C 接口或极简场景

适用场景:

  • C 语言接口/兼容
  • 系统级编程(信号处理、线程创建)
  • 绝对零开销且确定无捕获 lambda
  • 嵌入式/资源极度受限环境
//  推荐: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)

// C++23 std::function_ref:只读、非拥有、零分配
void process(std::function_ref<int(int)> op);

// 对比:
// std::function      : 拥有,可存储,有分配开销
// std::function_ref  : 不拥有,轻量,仅借用
// 模板               : 编译期确定,最优性能,无存储能力

总结建议

  1. 默认用模板:如果不需要存储,模板是零成本抽象的黄金标准
  2. 存储用 std::function:需要放入容器或类成员时
  3. C 接口用裸指针:被迫场景,或极端资源受限
  4. 关注 C++23std::function_ref 可能成为"需要引用但不存储"场景的最佳选择

核心原则:先尝试模板,遇到存储需求再降级到 std::function,裸指针作为最后手段。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com