万能调用神技 std::invoke

1. 什么是 std::invoke

在 C++17 之前,如果想调用一个函数,直接写 func(args);如果想调用一个对象的成员函数,需要写 obj.func(args) 或者 ptr->func(args)

但在写一些通用的代码(比如库代码、模板)时,编译器并不知道我们传进来的是一个普通函数、一个成员函数,还是一个像函数一样的对象(比如 lambda)。这时候,写法就非常麻烦了。

1
2
3
4
5
6
7
8
9
// 普通函数
func(args...);

// 成员函数
obj.func_ptr(args...);
obj->func_ptr(args...);

// 函数对象
func_obj(args...);

std::invoke 是 C++17 引入的一个函数模板,用于统一调用各种可调用对象。简单来说,std::invoke 是一个万能调用器,它提供了一种标准化的方式来调用函数、成员函数、函数对象等,无论它们是什么类型,这样可以让代码更加一致和可读。

1
2
3
4
5
6
7
#include <functional>  // std::invoke 头文件

template< class F, class... Args >
std::invoke_result_t<F, Args...> std::invoke(F&& f, Args&&... args);

// 等价于
std::invoke(可调用对象, 参数1, 参数2, ...);

该函数返回的就是调用结果,就像我们直接调用那个函数(可调用对象)一样。

2. 应用场景

2.1 调用普通函数和 Lambda

调用普通函数和Lambda这是最简单的用法,虽然看起来和直接调用差不多,但它是通用的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <functional>
#include <string>

// 1. 一个普通的自由函数
void print_hello(const std::string& name)
{
std::cout << "你好, " << name << "!" << std::endl;
}

// 2. 一个函数对象(仿函数)
struct Printer
{
void operator()(int x)
{
std::cout << "数字是: " << x << std::endl;
}
};

int main()
{
// --- 调用普通函数 ---
std::invoke(print_hello, "小明");

// --- 调用 Lambda 表达式 ---
auto lambda = [](double d) {
std::cout << "Pi 约等于: " << d << std::endl;
};
std::invoke(lambda, 3.14159);

// --- 调用函数对象 ---
Printer p;
std::invoke(p, 100); // 等价于 p(100);

return 0;
}

2.2 调用类的成员函数

调用类的成员函数这是 std::invoke 最闪光的地方。以前处理成员函数非常麻烦,因为需要把对象和函数指针拼起来。调用类的成员函数时,语法主要取决于对象是以引用、指针还是智能指针的形式传递

1
std::invoke(&ClassName::MethodName, instance, args...);
  • 参数1:成员函数的指针必须使用取址符 &
  • 参数2:类的实例对象或者对象引用、对象指针或者智能指针
  • 参数3…:传递给成员函数的参数

假设有如下类:

1
2
3
4
5
6
7
8
class Widget 
{
public:
void execute(int x)
{
std::cout << "Executing with " << x << std::endl;
}
};
  1. 使用对象实例或引用:最常用的方式。

    1
    2
    3
    4
    5
    6
    7
    Widget w;
    // 语法:&类名::方法名, 对象, 参数...
    std::invoke(&Widget::execute, w, 42);

    Widget w1;
    // 使用 std::ref(w) 强制引用传递
    std::invoke(&Widget::execute, std::ref(w1), 42);
  2. 使用对象指针:传递对象的指针(this 指针)。

    1
    2
    3
    Widget *ptr = new Widget;
    // 语法:&类名::方法名, 对象指针, 参数...
    std::invoke(&Widget::execute, ptr, 42);
  3. 使用智能指针:std::invoke 能够自动解引用 std::shared_ptrstd::unique_ptr

    1
    2
    3
    4
    5
    auto ptr1 = std::make_unique<Widget>();
    auto ptr2 = std::make_shared<Widget>();
    // 语法保持不变,不需要调用 .get()
    std::invoke(&Widget::execute, ptr1, 42);
    std::invoke(&Widget::execute, ptr2, 42);

当我们需要对对象列表进行排序时,经常需要根据某个字段排序。利用 std::invoke,可以写一个通用的比较器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

class Player
{
private:
std::string name;
int score;
public:
Player(std::string n, int s) : name(n), score(s) {}

// 为了方便打印,我们增加一个 getter
int getScore() const { return score; }
std::string getName() const { return name; }

int getEffectiveScore() const { return score; }
};

// 通用比较器
template <auto Member>
struct CompareBy
{
template <typename T>
bool operator()(const T& a, const T& b) const
{
return std::invoke(Member, a) < std::invoke(Member, b);
}
};

int main()
{
std::vector<Player> players = {
{"Bob", 100},
{"Alice", 200},
{"Charlie", 150}
};

std::cout << "排序前:" << std::endl;
for (const auto& p : players)
{
std::cout << p.getName() << ": " << p.getScore() << std::endl;
}
std::cout << "-------------------" << std::endl;

// 使用 std::sort 进行排序
// 这里传入了 CompareBy 的实例,并指定成员函数指针
std::sort(players.begin(), players.end(), CompareBy<&Player::getEffectiveScore>{});

std::cout << "排序后 (升序):" << std::endl;
for (const auto& p : players)
{
std::cout << p.getName() << ": " << p.getScore() << std::endl;
}

return 0;
}

2.3 调用重载的成员函数

如果成员函数被重载了(即有几个同名函数,参数不同),直接写 &ClassName::Func 会导致编译器无法确定是哪一个。必须显式转换为正确的函数指针类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <functional>

class Widget
{
public:
void process(int x) { std::cout << "Int: " << x << std::endl; }
void process(double x) { std::cout << "Double: " << x << std::endl; }
};

int main()
{
Widget w;

// 错误:无法推断是 process(int) 还是 process(double)
// std::invoke(&Widget::process, w, 10);

// 正确:指定函数指针类型
using FuncType = void (Widget::*)(int);
std::invoke(static_cast<FuncType>(&Widget::process), w, 10);

return 0;
}

2.4 访问类的数据成员

std::invoke 的核心设计理念是:统一调用可调用对象。而在 C++ 中,成员变量(非静态数据成员)也被视为一种可调用对象。当你使用 std::invoke 配合成员变量指针时,它实际上执行的是成员访问操作,而不是函数调用。对于成员变量,std::invoke 的行为等效于:

  • 对象或指针std::invoke(&Class::member, obj) 等价于 obj.member、obj->member
  • 对象引用std::invoke(&Class::member, std::ref(obj)) 等价于 obj.member

假设有一个通用的打印函数,它既可以处理成员函数(获取结果),也可以直接处理成员变量(直接读取值),完全不需要重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <functional>
#include <string>

struct User
{
std::string name;
int age;
std::string getRole() const { return "Admin"; }
};

// 通用打印函数:不关心 T 是成员变量还是成员函数
template <auto Member, typename Obj>
void printMember(const Obj& obj)
{
std::cout << "Value: " << std::invoke(Member, obj) << std::endl;
}

int main()
{
User u{"Alice", 30};

// 场景 A:直接“调用”成员变量 name
std::cout << "Direct Field: ";
printMember<&User::name>(u); // 输出: Value: Alice

// 场景 B:直接“调用”成员变量 age
std::cout << "Direct Field: ";
printMember<&User::age>(u); // 输出: Value: 30

// 场景 C:调用真正的成员函数
std::cout << "Method Call: ";
printMember<&User::getRole>(u); // 输出: Value: Admin

return 0;
}