深入理解 C++11 std::function
一、引言
在 C++11 之前,想把"函数"当作参数传递是一件很别扭的事:要么用函数指针(无法携带状态),要么用抽象基类 + 虚函数(代码冗长)。C++11 引入了 std::function,它是标准库对任意可调用对象的统一类型擦除封装,从此回调、事件、策略模式都变得优雅起来。
二、为什么需要 std::function
2.1 函数指针的局限
cpp
int add(int a, int b) { return a + b; }
// 函数指针只能指向普通函数,无法携带状态
int (*fp)(int, int) = add;
函数指针无法保存 lambda 捕获的变量,也无法保存带状态的仿函数。
2.2 模板的局限
cpp
template <typename F>
void run(F f) { f(); }
struct Handler { void onEvent(); };
模板虽然高效,但每个不同的 F 都会实例化一份新代码,而且无法把不同类型的可调用对象存进同一个容器:
cpp
std::vector<???> handlers; // 这里的类型怎么写?
std::function 正是为了解决这两个问题:统一的类型 + 统一的接口。
三、基本用法
cpp
#include <functional>
#include <iostream>
int add(int a, int b) { return a + b; }
int main() {
// 模板参数是签名:返回值(参数列表)
std::function<int(int, int)> f = add;
std::cout << f(1, 2) << '\n'; // 3
// 空 function 的判断
std::function<void()> g;
if (!g) std::cout << "g is empty\n";
}
注意签名是 int(int, int),不包含函数名。
四、可以包装哪些可调用对象
std::function 几乎可以包装一切能"用 () 调用"的东西。
4.1 普通函数与函数指针
cpp
int add(int a, int b) { return a + b; }
std::function<int(int,int)> f1 = add;
std::function<int(int,int)> f2 = &add; // 等价
4.2 Lambda 表达式(最常用)
cpp
int base = 100;
std::function<int(int)> f = [base](int x) { return x + base; };
std::cout << f(5); // 105
捕获列表中的状态被"打包"进了 std::function 内部。
4.3 仿函数(Functor)
cpp
struct Multiply {
int factor;
int operator()(int x) const { return x * factor; }
};
std::function<int(int)> f = Multiply{3};
std::cout << f(7); // 21
4.4 成员函数指针
cpp
struct Widget {
int value = 42;
int get() const { return value; }
};
Widget w;
// 注意:非静态成员函数第一个参数是对象
std::function<int(const Widget&)> f = &Widget::get;
std::cout << f(w); // 42
4.5 std::bind 的结果
cpp
#include <functional>
int add(int a, int b) { return a + b; }
std::function<int(int)> add10 = std::bind(add, 10, std::placeholders::_1);
std::cout << add10(5); // 15
五、与 std::bind 配合
std::bind 用来"绑定部分参数",但现代 C++ 更推荐用 lambda 代替 bind,因为 lambda 更易读、更容易被内联优化。
cpp
struct Server {
void onMessage(int id, const std::string& msg);
};
Server srv;
// bind 写法(参数顺序容易搞混)
auto cb1 = std::bind(&Server::onMessage, &srv, std::placeholders::_2,
std::placeholders::_1);
// lambda 写法(推荐,清晰)
auto cb2 = [&srv](int id, const std::string& msg) {
srv.onMessage(id, msg);
};
六、典型应用场景
6.1 回调注册表
cpp
class EventBus {
std::vector<std::function<void(const std::string&)>> listeners_;
public:
void subscribe(std::function<void(const std::string&)> cb) {
listeners_.push_back(std::move(cb));
}
void emit(const std::string& e) {
for (auto& cb : listeners_) cb(e);
}
};
不同类型的 lambda 可以存进同一个 vector,这正是类型擦除的威力。
6.2 策略模式
cpp
class Sorter {
public:
using Compare = std::function<bool(int, int)>;
void sort(std::vector<int>& v, Compare cmp) {
std::sort(v.begin(), v.end(), cmp);
}
};
Sorter s;
std::vector<int> v{3, 1, 2};
s.sort(v, [](int a, int b) { return a > b; }); // 降序
七、实现原理:类型擦除
std::function 的核心技术叫类型擦除(Type Erasure)。思路是:
- 定义一个抽象基类,声明
operator()对应的虚函数; - 用一个模板派生类保存具体的可调用对象;
std::function持有一个指向基类的指针。
一个极简的实现骨架:
cpp
template <typename R, typename... Args>
class MyFunction {
struct Base {
virtual ~Base() = default;
virtual R invoke(Args... args) = 0;
};
template <typename F>
struct Impl : Base {
F f;
explicit Impl(F fn) : f(std::move(fn)) {}
R invoke(Args... args) override { return f(std::forward<Args>(args)...); }
};
std::unique_ptr<Base> ptr_;
public:
template <typename F>
MyFunction(F f) : ptr_(new Impl<F>(std::move(f))) {}
R operator()(Args... args) { return ptr_->invoke(std::forward<Args>(args)...); }
};
真实的 std::function 在此基础上做了两点优化:
- 小对象优化(SBO):当可调用对象足够小(libstdc++ 中约 16 字节)且可无异常移动时,直接存放在对象内部的缓冲区,避免堆分配。
- 函数指针表:部分实现用函数指针代替虚表,进一步减少开销。
八、性能考量
std::function 不是零成本的,主要体现在:
| 开销来源 | 说明 |
|---|---|
| 类型擦除 | 编译器无法内联,无法做跨调用优化 |
| 间接调用 | 虚函数或函数指针跳转 |
| 潜在堆分配 | 捕获内容超过 SBO 缓冲区时 |
| 拷贝成本 | std::function 本身可拷贝,每次拷贝都要复制内部状态 |
8.1 性能对比示例
cpp
// 模板:可内联,无间接调用,最快
template <typename F>
void call_template(F f) { for (int i = 0; i < N; ++i) f(i); }
// std::function:有间接调用开销
void call_function(std::function<void(int)> f) {
for (int i = 0; i < N; ++i) f(i);
}
在热路径中,模板版本往往比 std::function 快数倍。
8.2 传递方式
cpp
void f1(std::function<void()> cb); // 值传递,可能拷贝
void f2(const std::function<void()>& cb); // 只读场景推荐
void f3(std::function<void()>&& cb); // 需要持有所有权时
九、常见陷阱
9.1 悬垂引用(最危险)
cpp
std::function<int()> make_bad() {
int local = 42;
return [&local]() { return local; }; // 危险!local 已销毁
}
std::function<int()> make_good() {
int local = 42;
return [local]() { return local; }; // 按值捕获,安全
}
9.2 空 function 调用
cpp
std::function<void()> f;
f(); // 抛出 std::bad_function_call
使用前务必检查:
cpp
if (f) f();
9.3 重载函数无法直接赋值
cpp
void foo(int);
void foo(double);
std::function<void(int)> f = foo; // 编译错误:二义性
std::function<void(int)> f = static_cast<void(*)(int)>(foo); // OK
// 或者用 lambda 包一层
std::function<void(int)> f = [](int x) { foo(x); };
9.4 可调用对象必须可拷贝
cpp
std::function<void()> f = [p = std::unique_ptr<int>(new int(1))]() {};
// C++11/14 编译错误:unique_ptr 不可拷贝
// C++14 起可用 std::ref 绕过(需自己保证生命周期)
// C++23 起应改用 std::move_only_function
9.5 生命周期问题:捕获 this
cpp
class Foo {
int x_ = 0;
public:
std::function<void()> getCallback() {
return [this]() { ++x_; }; // 若 Foo 先析构则悬垂
}
};
十、最佳实践
- 能用模板就用模板 :只在需要"统一类型"或"运行时存储"时才用
std::function。 - 优先按
const&传参,除非需要接管所有权。 - Lambda 优于
std::bind:可读性更好,优化空间更大。 - 注意捕获方式:跨作用域保存的回调一律按值捕获,或明确保证生命周期。
- 热路径慎用:高频调用的回调(如每帧渲染)考虑模板或函数指针。
- C++23 之后的新选择 :
std::move_only_function:支持只移动的可调用对象(如捕获unique_ptr的 lambda)。std::function_ref(C++26):非拥有引用,零开销,适合纯参数传递。
十一、总结
std::function 是 C++11 中最实用的工具之一,它用类型擦除把各种可调用对象统一成一种类型,让回调、事件、策略模式变得简洁优雅。
但它的代价是运行时的间接调用与潜在的堆分配。把它当作"需要运行时多态的可调用对象容器"来用,而不是"万能函数参数"------这是用好它的关键。
一句话记住:
需要统一类型 → 用
std::function;追求极致性能 → 用模板 /
auto。