C++ std::invoke()的用法(非常详细)
C++ 的开发人员,特别是实现库的开发人员,有时需要以统一的方式调用可调用对象。这可以是函数、指向函数的指针、指向成员函数的指针或函数对象,例如 std::bind、std::function、std::mem_fn 和 std::thread::thread。
C++17 定义了一个叫作 std::invoke() 的标准函数,它可以用提供的参数调用任何可调用对象。它的目的并不是取代对函数或函数对象的直接调用,但它在实现各种库函数的模板元编程中很有用。
为了举例说明如何在不同的上下文中使用 std::invoke(),我们将使用以下函数和类:
std::invoke() 可用于调用:
1) 自由函数:
2) 成员函数(通过指向成员函数的指针):
3) 数据成员:
4) 函数对象:
5) lambda 表达式:
实际上,应该在模板元编程中使用 std::invoke() 来调用具有任意数量参数的函数。为了举例说明这种情况,我们将给出 std::apply() 函数的一个可能实现,它是 C++17 标准库的一部分,它通过将元组的成员解包为函数的参数来调用函数:
给定一个函数,调用它的普遍方法是直接向它传递必要的参数,但是,也可以使用函数指针调用函数。函数指针的问题是定义指针的类型可能很麻烦。使用 auto 可以简化一些事情(如下面的代码所示),但在实践中,通常需要首先定义指向函数的指针的类型,然后定义一个对象并使用正确的函数地址初始化它。
下面是几个例子:
当需要通过类的实例对象调用类函数时,通过函数指针进行调用将变得更加麻烦。定义指向成员函数的指针并调用它的语法并不简单:
std::invoke() 的实现细节很复杂,但它的工作方式可以用简单的术语解释。假设调用的形式为 invoke(f, arg1, arg2,..., argN),需要考虑以下问题:
标准库还提供了一系列相关的类型特征,例如:
这些类型特征的 nothrow 版本验证调用可以在不抛出任何异常的情况下使用。
C++17 定义了一个叫作 std::invoke() 的标准函数,它可以用提供的参数调用任何可调用对象。它的目的并不是取代对函数或函数对象的直接调用,但它在实现各种库函数的模板元编程中很有用。
为了举例说明如何在不同的上下文中使用 std::invoke(),我们将使用以下函数和类:
int add(int const a, int const b) { return a + b; } struct foo { int x = 0; void increment_by(int const n) { x += n; } };
C++ std::invoke()的使用方法
invoke() 是一个可变参数函数模板,它接受可调用对象(作为第一个参数)和传递给调用的参数列表。std::invoke() 可用于调用:
1) 自由函数:
auto a1 = std::invoke(add, 1, 2); // a1 = 3
2) 成员函数(通过指向成员函数的指针):
foo f; std::invoke(&foo::increment_by, f, 10);
3) 数据成员:
foo f; auto x1 = std::invoke(&foo::x, f); // x1 = 0
4) 函数对象:
foo f; auto x3 = std::invoke(std::plus<>(), std::invoke(&foo::x, f), 3); // x3 = 3
5) lambda 表达式:
auto l = [](auto a, auto b) {return a + b; }; auto a = std::invoke(l, 1, 2); // a = 3
实际上,应该在模板元编程中使用 std::invoke() 来调用具有任意数量参数的函数。为了举例说明这种情况,我们将给出 std::apply() 函数的一个可能实现,它是 C++17 标准库的一部分,它通过将元组的成员解包为函数的参数来调用函数:
namespace details { template <class F, class T, std::size_t... I> auto apply(F&& f, T&& t, std::index_sequence<I...>) { return std::invoke( std::forward<F>(f), std::get<I>(std::forward<T>(t))...); } template <class F, class T> auto apply(F&& f, T&& t) { return details::apply( std::forward<F>(f), std::forward<T>(t), std::make_index_sequence< std::tuple_size_v<std::decay_t<T>>> {}); } }
C++ std::invoke()的工作原理
在了解 std::invoke() 如何工作之前,我们先快速了解一下如何调用不同的可调用对象。给定一个函数,调用它的普遍方法是直接向它传递必要的参数,但是,也可以使用函数指针调用函数。函数指针的问题是定义指针的类型可能很麻烦。使用 auto 可以简化一些事情(如下面的代码所示),但在实践中,通常需要首先定义指向函数的指针的类型,然后定义一个对象并使用正确的函数地址初始化它。
下面是几个例子:
// direct call auto a1 = add(1, 2); // a1 = 3 // call through function pointer int(*fadd)(int const, int const) = &add; auto a2 = fadd(1, 2); // a2 = 3 auto fadd2 = &add; auto a3 = fadd2(1, 2); // a3 = 3
当需要通过类的实例对象调用类函数时,通过函数指针进行调用将变得更加麻烦。定义指向成员函数的指针并调用它的语法并不简单:
foo f; f.increment_by(3); auto x1 = f.x; // x1 = 3 void(foo::*func)(int const) = &foo::increment_by; (f.*func)(3); auto x2 = f.x; // x2 = 6 auto func2 = &foo::increment_by; (f.*func2)(3); auto x3 = f.x; // x3 = 9不管这种调用看起来有多麻烦,实际的问题是编写能够以统一的方式调用这些类型的可调用对象的库组件(函数或类)。在实践中,这就是标准函数(如std::invoke())的好处。
std::invoke() 的实现细节很复杂,但它的工作方式可以用简单的术语解释。假设调用的形式为 invoke(f, arg1, arg2,..., argN),需要考虑以下问题:
-
如果 f 是指向 T 类成员函数的指针,那么调用等价于:
- (arg1.*f)(arg2,..., argN),arg1 是 T 的一个实例。
- (arg1.get().*f)(arg2,..., argN),arg1 是 reference_wrapper 的特化。
- ((*arg1).*f)(arg2,..., argN)。
-
如果 f 是指向 T 类数据成员的指针,并且只有一个参数,即调用的形式为 invoke(f, arg1),那么调用等价于:
- arg1.*f,arg1 是一个实例类 T。
- arg1.get().*f,arg1 是 reference_wrapper 的特化。
- (*arg1).*f。
- 如果 f 是一个函数对象,那么调用等价于 f(arg1, arg2,..., argN)。
标准库还提供了一系列相关的类型特征,例如:
- std::is_invocable 和 std::is_nothrow_invocable:确定是否可以使用提供的参数调用函数;
- std::is_invocable_r 和 std::is_nothrow_invocable_r:确定是否可以使用提供的参数调用函数,并生成可以隐式转换为指定类型的结果。
这些类型特征的 nothrow 版本验证调用可以在不抛出任何异常的情况下使用。