std::invoke 与 std::apply¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++17 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-26 |
编译器支持:GCC 7.1+ · Clang 16+ · MSVC 19.13+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 std::apply 把捕获的 tuple 展开成 SQL 参数绑定调用 | ev-drogon-apply-sql-binder-001 |
| 用 std::invoke 在 call_once 路径上调用任意可调用对象 | ev-abseil-invoke-once-callable-002 |
| 用 std::invoke 实现带返回类型归一化的通用调用器 | ev-abseil-invoke-r-003 |
本站证据页:ev-abseil-invoke-once-callable-002 · ev-abseil-invoke-r-003 · ev-drogon-apply-sql-binder-001
解决什么问题¶
泛型代码拿到一个可调用对象时,不知道它是函数指针、仿函数、成员函数指针还是 reference_wrapper。直接写 f(args...) 覆盖不了成员指针,std::bind 又会引入占位符、嵌套绑定和转发规则。拿到一个 tuple 时,想调用 f(a, b, c),还得手写下标和 std::get。
std::invoke 提供统一的调用语法,按标准 INVOKE 规则处理成员指针与引用包装;std::apply 把 tuple-like 对象的元素展开成实参。C++17 通过 N4169 引入 std::invoke,通过 P0220R1 引入 std::apply。两者把“怎么调用”从调用点抽出来,适合转发层、回调和类型擦除实现。
基础用法¶
#include <functional>
#include <iostream>
#include <tuple>
struct Counter {
int value;
int get() const { return value; }
};
int main() {
Counter counter{7};
std::cout << std::invoke(&Counter::get, counter) << '\n';
auto args = std::make_tuple(2, 3);
std::cout << std::apply([](int a, int b) { return a * b; }, args) << '\n';
}
std::invoke(&Counter::get, counter) 等价于 counter.get();成员指针、对象引用和参数由 invoke 统一拼装。std::apply 把 args 的两个元素依次传给 lambda,返回 6。
进阶用法¶
类型擦除层和分发表都依赖同一条性质:调用形式在编译期确定,但可调用对象的种类未知。std::invoke 把成员函数指针、数据成员指针、普通函数和仿函数都归一到同一种调用语法。
int (Counter::*getter)() const = &Counter::get;
int Counter::*member = &Counter::value;
std::cout << std::invoke(getter, counter) << ' '
<< std::invoke(member, counter) << '\n';
成员函数指针和对象分开保存后,调用点仍写 std::invoke(pointer, object, args...),不必为每种可调用对象写分支。std::apply 则处理另一侧:实参已经在一个 tuple-like 对象里,展开时按 std::get 的顺序传给可调用对象。
std::apply 展开的是 std::tuple_size 与 std::get 定义的结构;std::tuple、std::pair、std::array 都可以直接使用。自定义 tuple-like 类型要提供对应特化和 get,否则不能展开。
完整示例见 examples/cpp17/invoke_apply.cpp。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| Drogon | orm_lib/inc/drogon/orm/Criteria.h#L119-L128 |
用 std::apply 展开捕获的 tuple 并绑定 SQL 参数 |
参数个数在构造时确定,展开逻辑集中在一处,避免手写下标 |
| abseil-cpp | absl/base/call_once.h#L180-L190 |
用 std::invoke 调用任意 Callable 并转发参数 |
同步原语必须支持函数指针、仿函数和成员指针,同时保留值类别 |
| abseil-cpp | absl/functional/internal/any_invocable.h#L116-L124 |
用 std::invoke 实现返回类型归一化的调用器 |
类型擦除层用同一入口处理 void 与非 void 返回值 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。它们分别展示 tuple 展开、通用调用和类型擦除调用器。
陷阱与边界¶
std::apply 只接受 tuple-like 类型。 std::tuple、std::pair、std::array 可以直接用;自定义类型需要提供 std::tuple_size、std::tuple_element 和 get。普通聚合体不能直接展开。
值类别由 tuple 决定。 std::apply(f, tuple) 把元素作为左值传入;std::apply(f, std::move(tuple)) 把元素作为右值传入。转发层要根据调用方是否还持有 tuple 选择,移动后不要再次读取。
不要依赖实参求值顺序。 标准不保证展开后的实参按从左到右求值;带有相互依赖副作用的调用应拆开,或先算出结果再展开。
成员指针需要正确对象形式。 std::invoke 接受对象、对象指针或 reference_wrapper;数据成员指针返回对应成员的引用,注意对象生命周期。成员函数重载需要先 static_cast 到明确签名,invoke 无法替你选择重载。
std::bind 不是 std::invoke 的替代品。 bind 适合预先绑定部分参数,但嵌套绑定、占位符和转发规则容易出错;泛型转发通常用 lambda 或直接 invoke 更清楚。
异常会原样传播。 invoke 和 apply 不捕获异常,也不改变调用约定;回调边界需要自己处理错误与清理。
C++20 起可用于常量求值。 std::invoke 与 std::apply 在 C++20 成为 constexpr,可以在编译期展开 tuple;C++17 代码不能依赖这一点。
库联动¶
- Abseil:
call_once用std::invoke转发任意可调用对象;any_invocable、function_ref在类型擦除层复用它,并按返回值是否为void分支。 - Drogon:ORM 的参数绑定用
std::apply展开 tuple,协程映射器用std::apply把列名包展开进 SQL 拼接。 - 与
std::bind的分工:invoke/apply只负责“按统一规则调用”,不绑定参数;需要预绑定或占位符时仍用 lambda 或bind,但转发层应优先保持单一调用入口。
横向对比¶
| 方案 | 成员指针 | 转发值类别 | tuple 展开 | 主要代价 |
|---|---|---|---|---|
直接调用 f(...) |
否 | 由写法决定 | 否 | 泛型代码无法统一处理成员指针 |
std::invoke |
是 | 是 | 否 | 需要 <functional>;重载仍需显式选择 |
std::apply |
通过 invoke |
是 | 是 | 只支持 tuple-like;求值顺序不保证 |
std::bind |
部分 | 规则复杂 | 否 | 占位符与嵌套绑定可读性差 |
| lambda | 可手工处理 | 显式 | 否 | 需要为每种签名单独编写 |
std::mem_fn |
是 | 是 | 否 | 只处理成员指针,不覆盖普通可调用对象 |
跨标准演进:C++17 引入 std::invoke 与 std::apply;C++20 使两者可用于常量求值;C++23 增加 std::invoke_r,允许显式指定返回类型并处理可转换的返回值。