Skip to content

std::function 与可调用对象(Callable) ​

面试高频:「可调用对象有哪几种?」「std::function 和函数指针、lambda 有什么区别?」「std::function 为什么慢?」 核心是理解类型擦除(type erasure)。

一、可调用对象(Callable)有哪几种 ​

凡是能被 f(args...) 调用的东西,都是可调用对象。共四类:

  1. 普通函数
  2. 函数指针
  3. 重载了 operator() 的类对象(函数对象 / 仿函数)
  4. 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(高频对比) ​

维度函数指针lambdastd::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 可把它包装成自由函数式调用。

七、高频问答 ​

  1. 可调用对象有哪几种? —— 普通函数、函数指针、函数对象(重载 operator())、lambda。lambda 本质是函数对象。
  2. std::function 为什么能存不同类型的 callable? —— 类型擦除:对外统一 operator() 接口,对内间接调用真实对象。
  3. std::function 和函数指针的区别? —— 函数指针只能存同签名函数地址、不能捕获状态;std::function 能装 lambda/函数对象等一切 callable,但更慢、可能堆分配。
  4. lambda 和 std::function 是同一个东西吗? —— 不是。lambda 是匿名函数对象,std::function 是能装 lambda 的包装器。常见误解:std::function<void()> f = [](){...} 里 f 是 function,[](){...} 才是 lambda。
  5. 什么场景必须用 std::function? —— 需要统一类型存储异构 callable:回调表(map<string, function>)、任务队列(queue<function<void()>>,见 thread-pool)、观察者列表。

八、自测 ​

  1. 说出四种可调用对象,并举例。
  2. std::function<void()> 里装一个有 100 个捕获变量的 lambda,会发生堆分配吗?为什么?
  3. 为什么「优先用模板而不是 std::function」?性能差异在哪?
  4. lambda 能转成函数指针吗?什么条件下可以?

糌粑的面经笔记 · VitePress 版