std::function/模板/裸函数指针选型指南

选择 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  : 不拥有,轻量,仅借用
// 模板               : 编译期确定,最优性能,无存储能力

总结建议

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

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

相关推荐
柒儿吖1 小时前
基于 lycium 在 OpenHarmony 上交叉编译 utfcpp 完整实践
c++·c#·harmonyos
无聊的小坏坏2 小时前
一文讲通:二分查找的边界处理
数据结构·c++·算法
云深处@2 小时前
【C++11】包装器,智能指针
开发语言·c++
十五年专注C++开发2 小时前
CMake进阶:SelectLibraryConfigurations模块
c++·cmake·自动化构建
量子炒饭大师2 小时前
【C++入门】Cyber深度漫游者的初始链路——【类与对象】初始化成员列表
开发语言·c++·dubbo·类与对象·初始化成员列表
mmz12072 小时前
逆序对问题(c++)
c++·算法
化学在逃硬闯CS2 小时前
Leetcode110.平衡二叉树
数据结构·c++·算法·leetcode
谢铭轩2 小时前
题解:P8035 [COCI 2015/2016 #7] Otpor
c++·算法
阿猿收手吧!2 小时前
【C++】模块:告别头文件新时代
开发语言·c++