浏览知识库目录

C++

std::invoke

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::refstd::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_invocableinvoke_result 使用同一套语义。

一句话总结:

std::invoke 不是普通函数调用的替代品,而是泛型编程中“统一调用一切可调用对象”的基础设施。