C++ STL之std::function与可调用对象包装详解
一、用法速查
#include <functional>
#include <iostream>
int FreeFunc(int a) { return a * 2; }
struct FuncObj {
int operator()(int a) { return a + 1; }
};
int main() {
// 包装自由函数
std::function<int(int)> f1 = FreeFunc;
// 包装函数对象
std::function<int(int)> f2 = FuncObj{};
// 包装 Lambda
std::function<int(int)> f3 = [](int a) { return a - 1; };
// 包装 std::bind 结果
auto add = [](int x, int y) { return x + y; };
std::function<int(int)> f4 = std::bind(add, 10, std::placeholders::_1);
// std::mem_fn — 包装成员函数
struct S { int Get() const { return 42; } };
auto fn = std::mem_fn(&S::Get);
S s;
std::cout << fn(s) << "\n"; // 42
// C++17 std::not_fn — 取反
std::function<bool(int)> is_even = [](int x) { return x % 2 == 0; };
auto is_odd = std::not_fn(is_even);
std::cout << is_odd(3) << "\n"; // true
// C++20 std::bind_front — 绑定前 N 个参数
auto mul = [](int a, int b, int c) { return a * b * c; };
auto mul_2_3 = std::bind_front(mul, 2, 3);
std::cout << mul_2_3(4) << "\n"; // 24
}
二、底层原理
2.1 类型擦除
std::function 的核心机制是类型擦除。无论包装的是 Lambda、函数指针还是函数对象,对外都呈现统一的 std::function<Signature> 接口。实现上依赖桥接模式:
template <typename T>
struct FunctionImpl; // 特化版本,存储实际可调用对象
// 伪码示意:基类虚函数桥接
struct CallableBase { virtual ~CallableBase() = default; };
template <typename F, typename R, typename... Args>
struct CallableImpl : CallableBase {
F f;
R Invoke(Args... args) { return f(args...); }
};
2.2 小对象优化 vs 堆分配
- SBO(Small Buffer Optimization):标准库在
std::function内部预留固定大小缓冲区(通常是sizeof(void*) * 3左右)。当可调用对象大小不超过缓冲区时,直接就地构造,避免堆分配。 - 堆分配:当对象超出 SBO 容量(如捕获大量变量的 Lambda 或大型函数对象),
std::function退而使用operator new分配堆内存。
MSVC STL 的实现中,当可调用对象大小 ≤ 32 字节且对齐 ≤ alignof(max_align_t) 时走 SBO。GCC libstdc++ 类似,阈值约 24 字节。
2.3 std::bind 占位符与求值延迟
std::bind 返回一个函数对象,内部保存绑定的参数和占位符。占位符 _1, _2, ... 是命名空间 std::placeholders 中的特殊对象,调用时替换为实际传入的参数。
auto f = std::bind(add, 10, _1); // 不立即求值
f(5); // 此时才求值,等价于 add(10, 5)
延迟求值的好处是可以把部分参数固定后传递回调,但 C++17 后更推荐 Lambda 替代 bind,因为 Lambda 更直观且不会被 forward 语义坑。
2.4 std::mem_fn
将成员函数指针包装为可调用对象,支持引用、指针和智能指针调用:
auto get = std::mem_fn(&S::Get);
get(s); // s.Get()
get(&s); // s.Get()
get(sp); // sp->Get()
2.5 std::not_fn (C++17)
返回一个取反后的函数对象,等价于 [&](auto&&... args) { return !f(args...); }。优于 not1/not2,支持任意元数。
2.6 std::bind_front (C++20)
固定前 N 个参数,返回剩余参数由调用处提供的函数对象:
auto f = std::bind_front(func, a, b);
f(c, d); // 等价于 func(a, b, c, d)
与 std::bind 的区别:不处理占位符,语义更简单,性能更好。
三、Lambda vs std::function
Lambda 不是 std::function。Lambda 是编译器生成的匿名闭包类型,有自己的 operator()。std::function 是类型擦除的包装器。
Lambda 的 operator() 是 const 的(除非用 mutable),且默认 constexpr(C++17)。无捕获 Lambda 还可隐式转换为函数指针。
四、面试题精讲
1. std::function 可以包装哪些类型?
自由函数、函数指针、Lambda、函数对象(重载 operator() 的类/结构体)、std::bind 表达式、成员函数指针(配合 std::mem_fn)、以及另一个 std::function。
2. SBO 失效的条件是什么?
可调用对象大小超过缓冲区(约 24–32 字节),或对齐要求超过 max_align_t,或对象具有 non-trivial 的析构函数导致无法原地缓冲。此时触发堆分配。
3. std::bind 和 Lambda 在参数转发上有什么区别?
std::bind 会按值存储绑定的实参,除非用 std::ref/cref,容易导致 dangling reference。Lambda 用 auto&& 捕获时可完美转发。C++ 标准建议从 C++14 开始优先使用 Lambda 而非 bind。
4. 为什么 Lambda 不是 std::function?
Lambda 是编译期具体类型,有确定的 operator() 签名,零虚函数开销。std::function 是运行时包装器,有类型擦除和间接调用开销。把 Lambda 赋值给 std::function 会触发类型擦除并损失性能。
5. 无捕获 Lambda 为什么能转为函数指针?
标准规定无捕获 Lambda 定义了一个 operator() 和一个隐式的转换函数 operator Ret(*)(Params...)()。这使得无捕获 Lambda 可直接作为 C API 的回调。
6. std::not_fn 与 std::not1/not2 的根本区别?
not1/not2 要求可调用对象提供 argument_type 等嵌套类型(适配器模式),且只能处理一元/二元谓词。not_fn 利用可变参数模板和完美转发,支持任意元数。
7. std::function 的空状态如何检测和重置?
f == nullptr 或 !f 判断空状态,f = nullptr 或 f = {} 清空。调用空 std::function 会抛出 std::bad_function_call 异常。
8. std::bind_front 和 std::bind 在语义上最重要的区别是什么?
std::bind 支持占位符重新排列参数次序;std::bind_front 只做前缀绑定,不支持占位符重排,语义最简单,编译更快。
2万+



被折叠的 条评论
为什么被折叠?



