std::invoke与可调用对象:统一调用语法、成员函数指针
说实话,C++里可调用对象这块,早年挺让人头疼的。函数指针、仿函数、lambda、成员函数指针……每种东西的调用方式都不一样。你写个模板,想通用地调用一个东西,得写一堆 if constexpr 去判断类型。直到 C++17 引入了 std::invoke,这事儿才算真正清爽了。
我个人习惯把 std::invoke 理解为「万能调用器」。不管你是普通函数、lambda、还是成员函数指针,它都能一把搞定。今天咱们就把它掰开揉碎,看看它到底解决了什么问题。
可调用对象的四种形态
先捋一下,C++里哪些东西算「可调用对象」:
- 普通函数 / 函数指针:最传统的方式,
func(args) - 仿函数(函数对象):重载了
operator()的类实例 - Lambda 表达式:C++11 引入的匿名函数对象
- 成员函数指针 / 成员变量指针:指向类成员的指针,调用时需要绑定对象
前三种调用方式基本一致,都是 对象(参数)。但成员函数指针就特殊了——你得用 (obj.*ptr)(args) 或者 (pObj->*ptr)(args)。这语法,说实话,看着就累。
核心痛点:模板编程中,你没法统一写 f(args) 来调用所有可调用对象。因为成员函数指针的调用语法完全不同。
std::invoke 的诞生
C++17 的 std::invoke 就是为了解决这个问题的。它的签名长这样:
template<class F, class... Args>
decltype(auto) invoke(F&& f, Args&&... args);
它做的事情很简单:根据 f 的类型,自动选择正确的调用方式。如果是成员函数指针,它会帮你处理对象绑定;如果是普通可调用对象,直接调用。
我在项目中遇到过这样一个场景:写一个任务队列,需要存储各种类型的回调。有普通函数、有 lambda、还有成员函数。如果没有 std::invoke,我得写一堆重载或者用 std::bind 把成员函数转一下。有了它,直接存 std::function 配合 std::invoke,代码简洁多了。
成员函数指针的特殊性
成员函数指针这东西,很多新手容易懵。它本质上是一个偏移量,不是真正的函数地址。调用它必须绑定到一个具体对象上。
举个例子:
struct Widget {
void draw() const { /* ... */ }
int getValue() { return 42; }
};
// 声明成员函数指针
void (Widget::*pDraw)() const = &Widget::draw;
int (Widget::*pGet)() = &Widget::getValue;
Widget w;
// 传统调用方式
(w.*pDraw)();
int val = (w.*pGet)();
// 用 std::invoke
std::invoke(pDraw, w);
std::invoke(pGet, w);
你看,std::invoke 把 (w.*pDraw)() 这种别扭的写法,统一成了 invoke(pDraw, w)。而且它还能自动处理引用、指针、甚至 shared_ptr:
Widget* pw = new Widget();
std::shared_ptr<Widget> spw = std::make_shared<Widget>();
// 统统可以
std::invoke(pDraw, *pw); // 解引用
std::invoke(pDraw, pw); // 指针自动解引用
std::invoke(pDraw, spw); // shared_ptr 也支持
小技巧:std::invoke 对成员变量指针也有效。比如 int Widget::*pData = &Widget::data;,然后 std::invoke(pData, w) 就能拿到成员变量的值。这在写序列化库时特别有用。
invoke 的内部实现(简化版)
你想想看,std::invoke 到底是怎么做到「统一调用」的?说白了就是靠 if constexpr 和类型萃取。我写个简化版本,帮你理解它的核心逻辑:
namespace detail {
template<class F, class... Args>
auto invoke_impl(F&& f, Args&&... args)
-> decltype(std::forward<F>(f)(std::forward<Args>(args)...))
{
return std::forward<F>(f)(std::forward<Args>(args)...);
}
template<class Base, class T, class Derived>
auto invoke_impl(T Base::*pmd, Derived&& ref)
-> decltype(std::forward<Derived>(ref).*pmd)
{
return std::forward<Derived>(ref).*pmd;
}
template<class Base, class T, class Derived, class... Args>
auto invoke_impl(T Base::*pmf, Derived&& ref, Args&&... args)
-> decltype((std::forward<Derived>(ref).*pmf)(std::forward<Args>(args)...))
{
return (std::forward<Derived>(ref).*pmf)(std::forward<Args>(args)...);
}
}
template<class F, class... Args>
decltype(auto) invoke(F&& f, Args&&... args) {
return detail::invoke_impl(
std::forward<F>(f),
std::forward<Args>(args)...
);
}
嗯,这里要注意:标准库的实现比这个复杂得多,它还要处理 reference_wrapper、shared_ptr 等特殊情况。但核心思想就是——通过重载决议,把成员指针和普通可调用对象分开处理。
实战:用 invoke 简化回调系统
我记得有一次写一个事件分发器,需要支持注册各种类型的回调。用 std::invoke 配合 std::function,代码变得非常统一:
class EventDispatcher {
using Callback = std::function<void(const Event&)>;
std::unordered_map<int, std::vector<Callback>> handlers_;
public:
template<typename F>
void registerHandler(int eventType, F&& f) {
handlers_[eventType].emplace_back(std::forward<F>(f));
}
void dispatch(int eventType, const Event& e) {
auto it = handlers_.find(eventType);
if (it != handlers_.end()) {
for (auto& cb : it->second) {
std::invoke(cb, e); // 统一调用
}
}
}
};
// 使用示例
struct MyApp {
void onKeyPress(const Event& e) { /* ... */ }
};
EventDispatcher disp;
MyApp app;
// 注册普通函数
disp.registerHandler(1, [](const Event& e) { /* ... */ });
// 注册成员函数——不需要 bind,直接传指针和对象
disp.registerHandler(2, [&app](const Event& e) {
std::invoke(&MyApp::onKeyPress, app, e);
});
曾经踩过的坑:用 std::invoke 调用成员函数时,如果传的是临时对象,要小心生命周期。比如 std::invoke(&Widget::draw, Widget()),这个临时对象在 invoke 返回后就销毁了。如果你把结果存成引用,就悬空了。我建议传值或者用 std::shared_ptr 管理生命周期。
invoke 与 std::bind 的对比
很多人会问:有了 std::invoke,是不是 std::bind 就可以退休了?我的看法是:不完全。它们解决的是不同的问题。
| 特性 | std::invoke | std::bind |
|---|---|---|
| 核心目的 | 统一调用语法 | 参数绑定与延迟调用 |
| 是否生成新可调用对象 | 否,直接调用 | 是,返回绑定后的函数对象 |
| 对成员函数支持 | 原生支持,自动绑定对象 | 需要显式传对象参数 |
| 占位符支持 | 不支持 | 支持 _1, _2 等 |
| 性能 | 零开销抽象 | 可能有微小开销(绑定对象存储) |
说白了,如果你只是要「调用一个东西」,用 std::invoke。如果你要「生成一个延迟调用的函数对象」,用 std::bind 或者更推荐 lambda。
invoke 在模板元编程中的应用
在泛型代码里,std::invoke 的价值尤其明显。比如写一个通用的 for_each 来遍历容器并调用成员函数:
template<typename Container, typename MemberFunc>
void forEachCall(Container& c, MemberFunc f) {
for (auto& elem : c) {
std::invoke(f, elem); // 不管 f 是普通函数还是成员函数
}
}
std::vector<Widget> widgets;
// 调用普通函数
forEachCall(widgets, [](Widget& w) { w.draw(); });
// 调用成员函数——直接传指针
forEachCall(widgets, &Widget::draw);
你看,第二行直接传 &Widget::draw,std::invoke 自动帮我们把每个元素绑定上去。这种写法在 STL 算法里配合 std::mem_fn 也能做到,但 std::invoke 更通用。
SVG 知识结构图
下面这张图帮你梳理本章的核心脉络:
总结
std::invoke 不是什么炫技的特性,它解决的是一个很实在的问题——让可调用对象的调用方式统一起来。尤其是在模板编程中,你不需要再写一堆 if constexpr 去区分成员函数和普通函数了。
我个人建议:只要你的代码里出现了「需要通用地调用一个东西」的场景,优先考虑 std::invoke。它不会带来运行时开销,但能让你的泛型代码干净很多。配合 std::function 和 lambda,基本能覆盖 90% 的回调场景。
最后提醒一句:成员函数指针的 this 绑定,std::invoke 虽然帮你处理了,但你得确保传入的对象在调用时还活着。嗯,这个坑我替你们踩过了。
公众号:蓝海资料掘金营,微信deep3321