我写过一个“任务调度器”。
听起来很大。
其实就几十行。
存一个函数。
再存一堆参数。
之后再调用。
问题来了。
参数是 tuple。
你怎么把它喂回去?
当年:tuple 很方便,但“展开它”很难受
std::tuple 可以装一堆不同类型。
#include <tuple>
auto args = std::make_tuple(1, 2);
你想调用 add(1,2)。
但你手里是一个 tuple。
你早年要么手写 std::get<0>、std::get<1>。
要么写一堆模板递归来展开。
越写越像在写编译器。
线上啪一下:我为了支持不同 callable,写了一堆重载,最后漏了一种
更真实的是。
你想支持。
普通函数。
lambda。
成员函数。
成员变量指针。
你会发现。
“调用”在 C++ 里有很多种形态。
你靠肉眼写分支。
迟早会漏。
C++17:std::invoke,统一“怎么调用”
std::invoke 做的事很朴素。
给它一个可调用对象。
再给参数。
它会用正确的方式调用。
#include <functional>
int add(int a, int b) { return a + b; }
int x = std::invoke(add, 1, 2);
这看起来没区别。
但当你遇到成员函数指针。
区别就出来了。
#include <functional>
struct User {
int id() const { return 7; }
};
User u;
int x = std::invoke(&User::id, u);
你不需要记。
成员函数该怎么写 u.*pmf。
invoke 帮你统一了。
std::apply:把 tuple 当参数列表喂进去
apply 解决的是。
“我有一包参数在 tuple 里”。
“我想像正常调用一样传进去”。
#include <tuple>
#include <functional>
int add(int a, int b) { return a + b; }
auto args = std::make_tuple(1, 2);
int x = std::apply(add, args);
你可以把它理解成。
apply(f, tuple) 会变成 f(get0, get1, ...)。
apply + invoke:调度器/延迟调用的标配
你存了一个 callable。
再存一个 tuple 参数。
最后调用时。
auto run = [&](auto&& f, auto&& t) {
return std::apply([&](auto&&... xs) {
return std::invoke(f, std::forward<decltype(xs)>(xs)...);
}, t);
};
这段代码看着有点模板味。
但核心意思很简单。
把 tuple 展开。
再用 invoke 调。
你不用再为“这是函数还是成员函数”写分支。
关键结论
apply/invoke 的价值是。
把“展开参数”和“处理各种 callable 形态”的脏活收进标准库。
小结:当你在写泛型调用代码时,别再手搓展开
当年我们写 tuple 展开。
写成员函数调用。
写得很丑。
也很容易漏。
C++17 给了两个很小的工具。
让你的代码更像在说人话。
“把参数喂进去,然后调用。”