callable wrapper 与 std::invoke:怎样保存和调用行为 / Callable Wrappers and std::invoke
callable 不是一个具体类型。
它是一类“能被调用”的东西。现代 C++ 里,行为也可以像数据一样被传递、保存和组合。
1. 什么东西能被调用
普通函数:
void hello() {}
hello();函数指针:
void (*fp)() = &hello;
fp();lambda:
auto f = [] {
// do something
};
f();函数对象:
struct Printer {
void operator()() const {
// do something
}
};
Printer printer;
printer();它们都能写成类似:
callable();但类型完全不同。
function
function pointer
lambda closure type
function object type
member function pointercallable 这个词就是把这些“能被调用的东西”放在一起讨论。
2. 用模板接收 callable
最直接的方式是模板:
template <class F>
void run(F f) {
f();
}调用:
run([] {
// work
});优点:
保留 callable 的具体类型
编译器容易内联
没有 std::function 的类型擦除成本
可以接收 move-only callable缺点:
实现通常要放在头文件
每种 F 都可能实例化一份代码
错误信息可能更模板化
不能轻易把不同 callable 放进同一个变量模板适合“调用点和 callable 类型都在编译期已知”的场景。
3. std::function:把不同 callable 装进同一种类型
如果你需要保存一个回调:
#include <functional>
class Button {
public:
void on_click(std::function<void()> callback) {
callback_ = std::move(callback);
}
void click() {
if (callback_) {
callback_();
}
}
private:
std::function<void()> callback_;
};std::function<void()> 表示:
可以保存任何能按 void() 方式调用的 callable。它可以保存:
Button button;
button.on_click([] {
// lambda
});也可以保存函数指针、函数对象等。
图:
lambda / function pointer / functor
|
v
std::function<void()>
|
v
uniform call: callback()这叫类型擦除。
“擦除”不是说类型真的不存在。
而是说使用者不再需要知道内部保存的具体 callable 类型。
4. std::function 的代价
std::function 很方便,但有成本:
可能发生动态分配
调用可能是间接调用
通常要求 callable 可复制
会隐藏具体类型,影响内联机会
空 std::function 被调用会抛 bad_function_call所以选择时不要一律写 std::function。
如果只是立即调用一次:
template <class F>
void with_lock(F&& f) {
// lock
f();
// unlock
}模板通常更合适。
如果要把回调存到对象里,稍后调用,且需要统一类型:
std::function<void()> callback_;就更自然。
5. 成员函数指针为什么特殊
普通函数指针可以直接调用。
成员函数指针需要对象。
struct Widget {
void hello() {}
};
auto ptr = &Widget::hello;
Widget w;
(w.*ptr)();语法看起来很怪。
原因是成员函数不是独立函数。
它需要一个对象作为隐含的 this。
member function pointer
|
v
needs object
|
v
call on that object如果是对象指针:
Widget* p = &w;
(p->*ptr)();这也是成员函数指针语法难看的原因。
6. std::invoke 统一调用
std::invoke 可以统一处理多种 callable:
#include <functional>
std::invoke([] { });
std::invoke(&Widget::hello, w);
std::invoke(&Widget::hello, &w);它大致表达:
不管这是普通函数、lambda、函数对象、成员函数指针,
请按正确规则调用它。标准库里很多泛型设施会基于 invoke 的思想。
因为泛型代码不想手写一堆分支判断 callable 类型。
7. move-only callable
std::function 在 C++17/20 中通常要求保存的 callable 可复制。
但有些 lambda 捕获了 unique_ptr:
#include <memory>
auto ptr = std::make_unique<int>(42);
auto task = [p = std::move(ptr)] {
return *p;
};这个 lambda 是 move-only 的。
因为它内部拥有 unique_ptr。
这类 callable 不能放进传统 std::function。
C++23 有 std::move_only_function。
在 C++17/20 项目里,可能需要自定义包装器或改变设计。
这再次说明:callable 也会涉及所有权。
8. 本篇总结
选择 callable 表达方式时问:
只是立即调用,还是要保存?
是否需要保存不同具体类型?
是否需要复制 callable?
callable 是否 move-only?
性能是否敏感?
是否涉及成员函数指针?大致选择:
模板参数 F
编译期已知,性能好,适合立即调用
std::function
运行期保存不同 callable,接口统一,但有类型擦除成本
std::invoke
泛型代码中统一调用各种 callable
move_only_function 或自定义包装
保存 move-only callablecallable 的核心不是语法。
它是“行为作为对象”之后,类型、生命周期和所有权如何表达。