C++
std::invoke
发布于 2026年7月22日
std::invoke
std::invoke 是 C++17 引入的统一调用工具。它可以用同一种写法调用:
- 普通函数
- 函数指针
- Lambda
- 仿函数
- 成员函数指针
- 成员变量指针
需要包含:
#include <functional>
基本形式:
std::invoke(callable, args...);
可以把它理解为:
不管传进来的是什么可调用对象,都尝试按照正确的规则调用它。
1. 调用普通函数
#include <functional>
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
int result = std::invoke(add, 10, 20);
std::cout << result; // 30
}
对于普通函数,它基本等价于:
add(10, 20);
函数指针同样适用:
int (*function)(int, int) = &add;
int result = std::invoke(function, 10, 20);
2. 调用 Lambda
auto multiply = [](int a, int b) {
return a * b;
};
int result = std::invoke(multiply, 6, 7);
等价于:
int result = multiply(6, 7);
带捕获的 Lambda 也一样:
int factor = 10;
auto multiply = [factor](int value) {
return value * factor;
};
int result = std::invoke(multiply, 5); // 50
3. 调用仿函数
仿函数是重载了 operator() 的类:
struct Calculator {
int operator()(int a, int b) const
{
return a + b;
}
};
Calculator calculator;
int result = std::invoke(
calculator,
10,
20
);
等价于:
calculator(10, 20);
4. 调用成员函数指针
成员函数指针不能直接使用普通函数调用语法:
struct Person {
void print(int value)
{
std::cout << value;
}
};
auto function = &Person::print;
传统调用方式是:
Person person;
(person.*function)(42);
如果使用对象指针,则是:
Person* pointer = &person;
(pointer->*function)(42);
std::invoke 将它们统一成同一种写法:
Person person;
std::invoke(
&Person::print,
person,
42
);
使用对象指针:
std::invoke(
&Person::print,
&person,
42
);
这里的第一个额外参数是调用成员函数所需的对象,后面的参数才是成员函数自己的参数。
一般形式为:
std::invoke(
&Class::member_function,
object,
arguments...
);
静态成员函数同样可以调用,调用时不需要传入类对象:
// 静态成员函数
std::invoke(&Class::static_function, args...);
// 非静态成员函数
std::invoke(&Class::member_function, object, args...);
#include <functional>
#include <iostream>
class Calculator {
public:
static int add(int a, int b)
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
};
int main()
{
int result = std::invoke(
&Calculator::add,
10,
20);
int result2 = std::invoke(
&Calculator::multiply,
calculator, // 非静态成员函数需要对象
6,
7
);
std::cout << result; // 30
std::cout << result2; // 42
}
5. const 成员函数
struct Data {
int get() const
{
return 42;
}
};
const Data data;
int result = std::invoke(
&Data::get,
data
);
对象的 const 属性必须符合成员函数要求。
非 const 成员函数不能通过 const 对象调用:
struct Data {
void modify();
};
const Data data;
// 编译错误
std::invoke(&Data::modify, data);
6. 访问成员变量
std::invoke 不仅能调用成员函数,还能访问成员变量:
struct Person {
std::string name;
int age;
};
Person person{"Alice", 20};
int value = std::invoke(
&Person::age,
person
);
它等价于:
int value = person.age;
还可以修改成员:
std::invoke(&Person::age, person) = 21;
原因是 std::invoke 返回的是成员本身的引用:
int& age = std::invoke(
&Person::age,
person
);
使用对象指针也可以:
Person* pointer = &person;
std::invoke(&Person::age, pointer) = 22;
7. 支持 std::reference_wrapper
使用 std::ref 或 std::cref 包装对象时,std::invoke 会自动解包:
#include <functional>
struct Worker {
void run()
{
std::cout << "running";
}
};
Worker worker;
std::invoke(
&Worker::run,
std::ref(worker)
);
等价于:
worker.run();
这在泛型代码、线程参数传递和回调系统中非常方便。
8. 支持智能指针吗
对于成员函数指针和成员变量指针,std::invoke 也可以接受能够解引用得到目标对象的类型,例如智能指针:
#include <functional>
#include <memory>
struct Worker {
int run(int value)
{
return value * 2;
}
};
auto worker = std::make_unique<Worker>();
int result = std::invoke(
&Worker::run,
worker,
21
);
结果为:
42
这是因为 std::invoke 可以通过解引用获取 Worker 对象。
std::shared_ptr 同样适用:
auto worker = std::make_shared<Worker>();
std::invoke(&Worker::run, worker, 21);
9. 返回值类型
std::invoke 会保留被调用对象的真实返回类型,包括引用。
struct Storage {
int value = 0;
int& get()
{
return value;
}
};
Storage storage;
int& result = std::invoke(
&Storage::get,
storage
);
result = 42;
此时:
storage.value == 42
在泛型包装中可以使用 decltype(auto) 保留引用:
template<class F, class... Args>
decltype(auto) call(F&& function, Args&&... args)
{
return std::invoke(
std::forward<F>(function),
std::forward<Args>(args)...
);
}
如果写成:
auto call(...)
返回的引用可能被推导成值类型。
10. 完美转发
泛型代码中,通常应该配合 std::forward:
#include <functional>
#include <utility>
template<class F, class... Args>
decltype(auto) call(F&& function, Args&&... args)
{
return std::invoke(
std::forward<F>(function),
std::forward<Args>(args)...
);
}
这样可以保留:
- 左值和右值属性
const属性- 引用限定
- 移动语义
例如:
struct Handler {
void operator()() &
{
std::cout << "lvalue";
}
void operator()() &&
{
std::cout << "rvalue";
}
};
Handler handler;
call(handler); // lvalue
call(Handler{}); // rvalue
11. 与 std::invoke_result_t 的关系
二者通常一起使用:
using Result = std::invoke_result_t<F, Args...>;
std::invoke_result_t 在编译期推导:
std::invoke(function, args...)
会返回什么类型。
而 std::invoke 真正执行调用。
例如:
template<class F, class... Args>
auto async_call(F&& function, Args&&... args)
-> std::future<std::invoke_result_t<F&&, Args&&...>>;
可以简单记忆:
std::invoke // 执行调用
std::invoke_result_t // 推导调用结果类型
12. 与 std::is_invocable 配合
调用之前可以在编译期判断它是否有效:
template<class F, class... Args>
void try_call(F&& function, Args&&... args)
{
if constexpr (std::is_invocable_v<F&&, Args&&...>) {
std::invoke(
std::forward<F>(function),
std::forward<Args>(args)...
);
}
}
相关类型萃取包括:
std::is_invocable_v<F, Args...>
std::is_invocable_r_v<R, F, Args...>
std::is_nothrow_invocable_v<F, Args...>
std::is_nothrow_invocable_r_v<R, F, Args...>
分别用于判断:
- 能否调用
- 返回值能否转换成指定类型
- 调用是否为
noexcept - 能否调用、转换返回值并且不抛异常
13. 异常与 noexcept
std::invoke 不会捕获异常:
auto task = [] {
throw std::runtime_error("failed");
};
std::invoke(task); // 异常继续向外传播
泛型包装器可以根据调用对象自动推导 noexcept:
template<class F, class... Args>
decltype(auto) call(F&& function, Args&&... args) noexcept(
std::is_nothrow_invocable_v<F&&, Args&&...>)
{
return std::invoke(
std::forward<F>(function),
std::forward<Args>(args)...
);
}
C++17 也可以直接使用:
noexcept(
std::invoke(
std::declval<F>(),
std::declval<Args>()...
)
)
14. C++23 的 std::invoke_r
C++23 新增了 std::invoke_r<R>:
#include <functional>
double calculate()
{
return 42.8;
}
int value = std::invoke_r<int>(
calculate
);
它执行调用,并把结果转换成指定的 R:
value == 42
返回 void 时会丢弃结果:
std::invoke_r<void>(calculate);
它适合要求回调结果符合统一接口的泛型代码。
15. std::invoke 的核心规则
设第一个参数是 f。
普通可调用对象
大致执行:
f(args...)
成员函数指针
根据对象参数类型,大致执行以下之一:
(object.*f)(args...)
(pointer->*f)(args...)
成员变量指针
大致执行以下之一:
object.*f
pointer->*f
同时还会处理:
std::reference_wrapper
以及可解引用得到目标对象的类型。
16. 为什么不直接写 f(args...)
在只处理 Lambda、函数和仿函数时,可以直接写:
f(args...)
但它无法统一处理成员函数指针:
auto function = &Worker::run;
// 无效
function(worker, 42);
必须写成:
(worker.*function)(42);
使用 std::invoke:
std::invoke(function, worker, 42);
泛型代码因此不需要判断 F 究竟是哪一种可调用对象。
17. 常见错误
忘记包含头文件
#include <functional>
成员函数缺少对象参数
std::invoke(&Worker::run, 42); // 错误
应写成:
Worker worker;
std::invoke(&Worker::run, worker, 42);
对象生命周期已经结束
Worker* pointer = get_destroyed_worker();
std::invoke(&Worker::run, pointer); // 未定义行为
std::invoke 不管理对象生命周期。
泛型代码没有完美转发
template<class F, class... Args>
auto call(F function, Args... args)
{
return std::invoke(function, args...);
}
这种写法会产生复制,并丢失参数的引用和值类别信息。
更好的写法是:
template<class F, class... Args>
decltype(auto) call(F&& function, Args&&... args)
{
return std::invoke(
std::forward<F>(function),
std::forward<Args>(args)...
);
}
使用 auto 丢失引用
auto result =
std::invoke(&Storage::value, storage);
这里 auto 会得到一个值拷贝。如果需要保留引用,应使用:
decltype(auto) result =
std::invoke(&Storage::value, storage);
或者:
auto& result =
std::invoke(&Storage::value, storage);
18. 典型应用场景
std::invoke 常见于:
- 通用回调包装器
- 线程池
- 事件系统
- 任务调度器
- 泛型算法
- 依赖注入
- 成员函数与普通函数的统一适配
例如任务执行器:
template<class F, class... Args>
auto execute(F&& function, Args&&... args)
{
using Result =
std::invoke_result_t<F&&, Args&&...>;
std::packaged_task<Result()> task(
[
function =
std::forward<F>(function),
... arguments =
std::forward<Args>(args)
]() mutable -> Result {
return std::invoke(
std::move(function),
std::move(arguments)...
);
}
);
auto future = task.get_future();
task();
return future;
}
19. 总结
最核心的用法是:
std::invoke(function, arguments...);
相关工具可以一起记:
// 执行统一调用
std::invoke(f, args...)
// 推导返回类型
std::invoke_result_t<F, Args...>
// 判断能否调用
std::is_invocable_v<F, Args...>
// 判断能否调用且返回值可转换为 R
std::is_invocable_r_v<R, F, Args...>
// C++23:调用并转换结果
std::invoke_r<R>(f, args...)
std::invoke 的真正价值不在于替代普通的 f(args...),而在于泛型代码中统一普通函数、函数对象和成员指针的调用方式。
std::invoke存在的真正意义
直接调用并不统一
普通函数、Lambda、仿函数可以这样调用:
f(args...);
但成员函数指针必须这样调用:
(object.*f)(args...);
如果对象是指针,又要写成:
(pointer->*f)(args...);
成员变量指针则是:
object.*member;
pointer->*member;
这意味着泛型代码如果不用 std::invoke,就要判断 f 到底是什么。
不使用 std::invoke
下面的包装器可以调用普通函数、Lambda 和仿函数:
template<class F, class... Args>
decltype(auto) call(F&& f, Args&&... args)
{
return std::forward<F>(f)(
std::forward<Args>(args)...
);
}
但不能调用成员函数指针:
struct Worker {
int run(int value)
{
return value * 2;
}
};
Worker worker;
// 编译错误
call(&Worker::run, worker, 21);
原因是成员函数指针不能写成:
f(worker, 21);
使用 std::invoke
template<class F, class... Args>
decltype(auto) call(F&& f, Args&&... args)
{
return std::invoke(
std::forward<F>(f),
std::forward<Args>(args)...
);
}
同一个 call 可以处理所有情况:
int add(int a, int b)
{
return a + b;
}
struct Multiply {
int operator()(int a, int b) const
{
return a * b;
}
};
struct Worker {
int value = 10;
int run(int x)
{
return value + x;
}
};
Worker worker;
call(add, 1, 2); // 普通函数
call(Multiply{}, 3, 4); // 仿函数
call([] { return 42; }); // Lambda
call(&Worker::run, worker, 5); // 成员函数指针
call(&Worker::value, worker); // 成员变量指针
这才是 std::invoke 的核心价值。
它统一处理对象的不同形式
对于成员函数指针:
&Worker::run
std::invoke 可以接受多种对象形式:
Worker worker;
std::invoke(&Worker::run, worker, 1);
std::invoke(&Worker::run, &worker, 1);
std::invoke(&Worker::run, std::ref(worker), 1);
auto pointer = std::make_shared<Worker>();
std::invoke(&Worker::run, pointer, 1);
如果自己实现,就需要分别处理:
(object.*member)(args...);
(pointer->*member)(args...);
reference_wrapper.get();
(*smart_pointer).*member;
std::invoke 把这些规则封装好了。
通常只有以下情况值得使用 std::invoke:
- 编写函数模板;
- 封装回调;
- 实现线程池或任务系统;
- 可调用对象可能是成员指针;
- 希望与
is_invocable、invoke_result使用同一套语义。
一句话总结:
std::invoke 不是普通函数调用的替代品,而是泛型编程中“统一调用一切可调用对象”的基础设施。