Appearance
std::function 与可调用对象(Callable)
面试高频:「可调用对象有哪几种?」「std::function 和函数指针、lambda 有什么区别?」「std::function 为什么慢?」 核心是理解类型擦除(type erasure)。
一、可调用对象(Callable)有哪几种
凡是能被 f(args...) 调用的东西,都是可调用对象。共四类:
- 普通函数
- 函数指针
- 重载了
operator()的类对象(函数对象 / 仿函数) - lambda 表达式(编译器生成的匿名类,本质是第 3 类的语法糖)
函数指针复习
函数名可以取地址,函数指针本身也可调用:
cpp
int add(int a, int b) { return a + b; }
int (*padd)(int, int) = &add; // 函数指针
std::cout << (*padd)(3, 4); // 解引用再调用
std::cout << padd(3, 4); // 也可直接调用,(*p)(...) 等价 p(...)二、std::function:通用函数包装器
定义在 <functional>。std::function<Ret(Args...)> 能存储任意可调用对象(只要它能被 Args... 调用并返回 Ret):
cpp
std::function<double(double)> f1 = poly; // 函数对象(重载 operator())
std::function<void()> f2 = []() { std::cout << 42; }; // lambda
std::function<int(int,int)> f3 = &add; // 函数指针
std::function<double(double,double)> f4 = std::plus<>{}; // 标准库函数对象经典用法:把「运算符」映射到「函数」,实现数据驱动的计算器:
cpp
std::map<char, std::function<double(double,double)>> funcMap{
{'+', std::plus<>{}}, // 标准库函数对象
{'-', std::minus<>{}},
{'*', [](double a, double b){ return a*b; }}, // lambda
{'/', Divides{}} // 自定义函数对象
};
double lhs, rhs; char op;
std::cin >> lhs >> op >> rhs;
std::cout << funcMap[op](lhs, rhs) << '\n';一个
std::function容器里能同时装函数指针、lambda、函数对象——因为它们的类型虽然不同,但调用签名相同,被std::function统一擦除成了同一个类型。
三、核心机制:类型擦除(type erasure)
问题:lambda 的类型是编译器生成的匿名类,每个 lambda 类型都不同;函数指针是 int(*)(int,int);函数对象是自定义类。它们类型各异,怎么放进同一个 map<char, std::function<...>>?
答案:std::function 用「类型擦除」——对外暴露统一的接口 Ret operator()(Args...),对内用虚函数/函数指针间接调用真正的可调用对象。它不关心具体是什么类型,只关心「能不能用 Args... 调用并返回 Ret」。
本质可以用一个简化版理解:
cpp
// 概念示意(真实实现复杂得多,含小对象优化 SBO)
template <typename Ret, typename... Args>
class function {
// 内部一个基类接口 + 模板派生类,各自持有真正的 callable
// 通过虚调用转发到具体 callable 的 operator()
};这正是多态的一种「值语义」实现:不用继承,也能统一不同类型。
std::any、std::variant背后也是类型擦除/访问者的思想。
四、std::function vs 函数指针 vs lambda(高频对比)
| 维度 | 函数指针 | lambda | std::function |
|---|---|---|---|
| 本质 | 指针(一个地址) | 匿名函数对象(编译器生成) | 包装器(类型擦除) |
| 能捕获状态 | ✗(无捕获) | ✅ 可捕获 [&]/[=] | ✅ 装任何 callable |
| 能否存进容器 | 只能存同一签名的指针 | 每个 lambda 类型不同,需 auto | ✅ 统一类型,可存 map/vector |
| 运行时开销 | 几乎零(直接跳转) | 可能内联,零开销 | 间接调用(虚函数式)+ 可能堆分配,最慢 |
| 使用场景 | 与 C 接口交互、无状态回调 | 传参/临时回调(配合模板) | 需要「统一存储」不同类型的 callable |
关键结论:
- lambda 最快:无捕获 lambda 可内联,甚至转成函数指针;有捕获则是个带状态的小对象,配合
template也常被内联。 - std::function 最慢:因为是类型擦除,调用要走一次间接跳转(类似虚函数),且若 callable 超过「小对象优化」阈值会堆分配。
- 因此:能用模板(
template <typename F>)就别用std::function;只有当你「必须把不同类型可调用对象存进同一个容器/成员」时,才用std::function。
五、性能细节:小对象优化(SBO)
std::function 内部通常预留一小块内联缓冲区(类似 std::string 的 SSO):
- callable 足够小(如无捕获 lambda、普通函数指针)→ 直接存进内联缓冲区,不堆分配。
- callable 较大(捕获了很多变量的 lambda)→ 堆分配存储。
所以「std::function 一定堆分配」是错的——小对象走 SBO 不分配,但间接调用的开销始终在。
六、std::bind 与成员函数指针(了解)
std::bind(f, args...):把函数和部分参数「绑定」,返回一个新的可调用对象(占位符std::placeholders::_1表示待填参数)。C++14 起大多场景被 lambda 取代(lambda 更可读、可内联)。- 成员函数指针也是可调用对象:
&Class::method需要配合对象调用((obj.*pmf)(args)或(ptr->*pmf)(args)),std::mem_fn可把它包装成自由函数式调用。
七、高频问答
- 可调用对象有哪几种? —— 普通函数、函数指针、函数对象(重载
operator())、lambda。lambda 本质是函数对象。 - std::function 为什么能存不同类型的 callable? —— 类型擦除:对外统一
operator()接口,对内间接调用真实对象。 - std::function 和函数指针的区别? —— 函数指针只能存同签名函数地址、不能捕获状态;std::function 能装 lambda/函数对象等一切 callable,但更慢、可能堆分配。
- lambda 和 std::function 是同一个东西吗? —— 不是。lambda 是匿名函数对象,std::function 是能装 lambda 的包装器。常见误解:
std::function<void()> f = [](){...}里f是 function,[](){...}才是 lambda。 - 什么场景必须用 std::function? —— 需要统一类型存储异构 callable:回调表(
map<string, function>)、任务队列(queue<function<void()>>,见 thread-pool)、观察者列表。
八、自测
- 说出四种可调用对象,并举例。
std::function<void()>里装一个有 100 个捕获变量的 lambda,会发生堆分配吗?为什么?- 为什么「优先用模板而不是 std::function」?性能差异在哪?
- lambda 能转成函数指针吗?什么条件下可以?