C++23 std::invoke_r:调用可调用 (Callable) 对象 (P2136R3)

文章目录

引言

在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_rstd::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_rstd::invoke的基础上,增加了指定返回类型的功能,并且可以自然地丢弃返回值。

使用场景差异

  • std::invoke适用于需要统一调用各种不同类型可调用对象的场景,而不关心返回值的具体类型。
  • std::invoke_r适用于需要指定返回类型或丢弃返回值的场景。

结论

C++23引入的std::invoke_r是对std::invoke的进一步扩展,它提供了指定返回类型和丢弃返回值的功能,使得在处理可调用对象时更加灵活。在实际开发中,我们可以根据具体的需求选择使用std::invokestd::invoke_r,以提高代码的可读性和可维护性。

相关推荐
Kairo_0136 分钟前
在 API 模拟阶段:Apipost vs. Faker.js vs. Postman —— 为什么 Apipost 是最优选择
开发语言·javascript·postman
Once_day1 小时前
研发效率破局之道阅读总结(4)个人效率
开发语言·研发效能·devops
痕5171 小时前
如何在idea中写spark程序。
开发语言
橙子199110161 小时前
请简述一下什么是 Kotlin?它有哪些特性?
android·开发语言·kotlin
martian6652 小时前
信创系统图形界面开发指南:技术选择与实践详解
开发语言·科技·系统架构·系统安全·创业创新
我命由我123452 小时前
STM32 开发 - stm32f10x.h 头文件(内存映射、寄存器结构体与宏、寄存器位定义、实现点灯案例)
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
ghost1432 小时前
C#学习第20天:垃圾回收
开发语言·学习·c#
一眼青苔3 小时前
conda添加新python版本环境,如何激活和销毁
开发语言·python·conda
Pseudo…3 小时前
web技术与Nginx网站服务
开发语言·php