
文章目录
引言
在C++的发展历程中,对于可调用对象的处理一直是一个重要的话题。从早期不同类型可调用对象调用语法的不一致,到C++17引入std::invoke
提供统一的调用语法,再到C++23推出std::invoke_r
,每一次的改进都在提升语言的表达能力和编程的便利性。本文将深入探讨C++23中的std::invoke_r
,包括其定义、使用场景、与之前版本的对比等内容。
背景知识回顾
可调用对象
在C++的世界里,"可调用"(Callable)是一个宽泛的概念,涵盖了多种实体,它们都可以像函数一样被"调用"以执行某些操作:
- 普通函数指针 :如
void(*ptr)(int);
- Lambda表达式 :如
auto lambda = [](int x){ return x * 2; };
- 函数对象(Functor) :重载了
operator()
的类实例。 - 指向(非静态)成员函数的指针 :如
int MyClass::*mem_func_ptr)(int);
- 指向(非静态)成员变量的指针 :如
int MyClass::*mem_data_ptr;
(std::invoke
可用于获取其值) - (静态)成员函数:可以像普通函数一样通过指针或直接调用。
C++17的std::invoke
在C++17之前,调用不同类型的可调用对象需要使用不同的语法,比如直接调用函数、使用类对象的运算符重载操作符()
来调用函数对象、使用成员函数指针来调用类成员函数等等。这些调用方式虽然能用,但是不够灵活,给编写和维护代码带来了困扰。
std::invoke
是C++ 17标准库中引入的一个函数模板,它的引入就是为了解决这个问题,它提供了一种统一的调用语法,无论是调用普通函数、函数指针、类成员函数指针、仿函数、std::function
、类成员还是lambda表达式 ,都可以使用相同的方式进行调用。
std::invoke
的语法如下:
cpp
template <typename Fn, typename... Args>
decltype(auto) invoke(Fn&& fn, Args&&... args);
它接受一个可调用对象fn
和相应的参数args...
,并返回调用结果。例如:
cpp
#include <functional>
#include <iostream>
#include <type_traits>
struct Foo{
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_ + i << '\n'; }
int num_;
};
void print_num(int i){
std::cout << i << '\n';
}
struct PrintNum{
void operator()(int i) const
{
std::cout << i << '\n';
}
};
int main(){
// 调用自由函数
std::invoke(print_num, -9);
// 调用 lambda
std::invoke([]() { print_num(42); });
// 调用成员函数
const Foo foo(314159);
std::invoke(&Foo::print_add, foo, 1);
// 调用(访问)数据成员
std::cout << "num_:" << std::invoke(&Foo::num_, foo) << '\n';
// 调用函数对象
std::invoke(PrintNum(), 18);
return 0;
}
通过std::invoke
,我们可以在不关心可调用对象的具体类型的情况下进行调用,提高了代码的灵活性和可读性。它尤其适用于泛型编程中需要以统一方式调用各种可调用对象的场景,例如使用函数指针或成员函数指针作为模板参数的算法或容器等。
std::invoke_r的诞生
提案背景
当std::invoke
在N4169中被引入时,invoke<R>
从提案中被移除,当时认为这种形式是不必要的,理由是在TR1实现中,结果类型是使用result_of
协议确定的,或者必须在调用端指定,而在C++11引入类型推导后,它就变得过时了。
然而,随着时间的推移,情况发生了变化:
- 2015年,LWG 2420被应用到工作草案中,
INVOKE(f, args..., void)
丢弃返回值的能力得到确认和规范。 - 2016年,本文的作者提出了LWG 2690,提议引入
std::invoke<R>
。N4169的作者对此问题进行了评论,确认invoke<R>
的缺失主要是由于论文的修订同时发布以及INVOKE(f, args..., void)
的额外特殊语义。 - 2017年,
INVOKE(f, args..., void)
在P0604R0中获得了当前的拼写INVOKE<R>(f, args...)
。在同一篇论文中,所有新的调用特性都有了允许指定返回类型的_r
变体。 - 2018年,
std::visit<R>
被添加到工作草案中,INVOKE<R>
的实用性不断受到关注。
std::invoke_r的定义
std::invoke_r
定义于头文件<functional>
,其原型如下:
cpp
template< class R, class F, class... Args >
constexpr R
invoke_r( F&& f, Args&&... args ) noexcept(/* 见下方 */);
它通过可调用对象f
,以参数args
调用,如同INVOKE<R>(std::forward<F>(f), std::forward<Args>(args)...)
。此重载仅在std::is_invocable_r_v<R, F, Args...>
为true
时参与重载决议。
参数和返回值
- 参数 :
f
:可调用对象,将被调用。args
:传递给f
的参数。
- 返回值 :
f
返回的值,隐式转换为R
,如果R
不是(可能带有cv限定的)void
类型。否则无返回值。
异常说明
noexcept
说明:noexcept(std::is_nothrow_invocable_r_v<R, F, Args...>)
std::invoke_r的使用场景
指定返回类型
invoke_r<R>(...)
可以指定可调用对象的返回类型,这是std::invoke(...)
所不具备的功能。例如:
cpp
#include <functional>
#include <iostream>
auto add = [](int x, int y) { return x + y; };
auto ret = std::invoke_r<float>(add, 11, 22);
static_assert(std::is_same<decltype(ret), float>());
std::cout << ret << '\n';
在这个例子中,我们使用std::invoke_r<float>
调用lambda表达式add
,并将返回值转换为float
类型。
丢弃返回值
在一个允许指定返回类型或完整签名的调用转发器中,将void
作为返回类型可以自然地丢弃返回值,这由std::is_invocable_r
和std::is_nothrow_invocable_r
所暗示。例如:
cpp
#include <functional>
#include <iostream>
void print_num(int i){
std::cout << i << '\n';
}
std::invoke_r<void>(print_num, 44);
在这个例子中,我们使用std::invoke_r<void>
调用print_num
函数,丢弃了函数的返回值。
std::invoke_r与std::invoke的对比
功能差异
std::invoke
提供了统一的调用语法,无论可调用对象的类型是什么,都可以使用同一种方式进行调用,但它不能指定返回类型。std::invoke_r
在std::invoke
的基础上,增加了指定返回类型的功能,并且可以自然地丢弃返回值。
使用场景差异
std::invoke
适用于需要统一调用各种不同类型可调用对象的场景,而不关心返回值的具体类型。std::invoke_r
适用于需要指定返回类型或丢弃返回值的场景。
结论
C++23引入的std::invoke_r
是对std::invoke
的进一步扩展,它提供了指定返回类型和丢弃返回值的功能,使得在处理可调用对象时更加灵活。在实际开发中,我们可以根据具体的需求选择使用std::invoke
或std::invoke_r
,以提高代码的可读性和可维护性。