C++17之std::invoke: 使用和原理探究(全)

2024-03-01 23:28

本文主要是介绍C++17之std::invoke: 使用和原理探究(全),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1.概述

2.辅助类

3.原理分析

4.总结


1.概述

        在之前的 C++ 版本中,要调用不同类型的可调用对象,需要使用不同的语法,例如使用函数调用运算符 () 来调用函数或函数指针,使用成员访问运算符 -> 或 . 来调用成员函数。这样的语法差异导致了代码的冗余和不一致,给编写和维护代码带来了困扰。

        std::invoke 是 C++17标准库中引入的一个函数模板,它的引入就是为了解决这个问题,它提供了一种统一的调用语法,无论是调用普通函数、函数指针、类成员函数指针、仿函数、std::function、类成员还是lambda表达式,都可以使用相同的方式进行调用。

        std::invoke 的语法如下:

template <typename Fn, typename... Args>
decltype(auto) invoke(Fn&& fn, Args&&... args);

它接受一个可调用对象 fn 和相应的参数 args...,并返回调用结果。例如:

#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);// 调用 lambdastd::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);#if defined(__cpp_lib_invoke_r)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<void>(print_num, 44);
#endif
}

可能的输出:

-9
42
314160
num_:314159
18
33
44

        通过 std::invoke,我们可以在不关心可调用对象的具体类型的情况下进行调用,提高了代码的灵活性和可读性。它尤其适用于泛型编程中需要以统一方式调用各种可调用对象的场景,例如使用函数指针或成员函数指针作为模板参数的算法或容器等。

2.辅助类

        阅读后面的内容,你必须事先了解以下内容:

        1.constexpr

        2.std::is_base_of_v

        3.std::remove_cv_t

        4.std::ref和std::cref

        5.std::is_member_function_pointer

        6.std::is_member_object_pointer_v

        7.左值和右值

3.原理分析

        从上面的例子我们可以猜想,std::invoke的实现应该是根据传入的参数Fn来判断出Fn是否为可调用对象(Callable),常见的可调用对象有:

  • function 
  • member function
  • function object
  • lambda expression
  • bind expression
  • std::function

如果是可调用对象,那肯定也需要分析出是那种可调用对象,C++涉及到的可调用对象有:

        1.普通函数,保证了对C的兼容。如:void  func(int x, int y);

        2.函数指针。和数组名一样,函数名即为函数指针。如:

	typedef void(*FType)(int); //定义一个函数指针类型Ftypevoid func(FType fn, int x) {fn(x);}

        3.类成员变量和成员函数

	class CTestabcd{public:inline int func(int a, int b) { return a + b; }public:int  m_i;};using TestFunc = int (CTestabcd::*)(int, int);using TestMember = int(CTestabcd::*);TestFunc gTestFunc = &CTestabcd::func;TestMember gTestMember = &CTestabcd::m_i;

        4.仿函数(函数对象),即重载了operator()运算符的类对象,如:

    template <class _Ty = void>struct less {_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty _FIRST_ARGUMENT_TYPE_NAME;_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty _SECOND_ARGUMENT_TYPE_NAME;_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool _RESULT_TYPE_NAME;_NODISCARD constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const {return _Left < _Right;}};

        std::bind绑定,它是STL的配接器,用于创建一个可调用的对象,对象里面重载了operator(),也是运用了仿函数的思想,如: 

#include <iostream>  
#include <functional>  
#include <thread>  
#include <chrono>  
#include <vector>  
#include <algorithm>  void print_sum(int x, int y) {  std::cout << x + y << "\n";  
}  int main() {  std::vector<int> nums = {1, 2, 3, 4, 5};  auto bound_sum = std::bind(print_sum, std::placeholders::_1, 5);  // 绑定第二个参数为 5。  std::for_each(nums.begin(), nums.end(), bound_sum);  // 对于每个元素,输出它与 5 的和。  return 0;  
}

        5.lambda表达式,如:

auto f = [] { return "hello world"; }; 
cout << f() << endl; // 输出:hello world

        6.std::function, 如:

#include <iostream>
#include <functional>// std::function
std::function<int(int, int)> SumFunction;// 普通函数
int func_sum(int a, int b)
{return a + b;
}class Calcu
{
public:int base = 20;// 类的成员方法,参数包含this指针int class_func_sum(const int a, const int b) const { return this->base + a + b; };// 类的静态成员方法,不包含this指针static int class_static_func_sum(const int a, const int b) { return a + b; };
};// 仿函数
class ImitateAdd
{
public:int operator()(const int a, const int b) const { return a + b; };
};// lambda函数
auto lambda_func_sum = [](int a, int b) -> int { return a + b; };// 函数指针
int (*func_pointer)(int, int);int main(void) 
{int x = 2; int y = 5;// 普通函数SumFunction = func_sum;int sum = SumFunction(x, y);std::cout << "func_sum:" << sum << std::endl;// 类成员函数Calcu obj;SumFunction = std::bind(&Calcu::class_func_sum, obj, std::placeholders::_1, std::placeholders::_2); // 绑定this对象sum = SumFunction(x, y);std::cout << "Calcu::class_func_sum:" << sum << std::endl;// 类静态函数SumFunction = Calcu::class_static_func_sum;sum = SumFunction(x, y);std::cout << "Calcu::class_static_func_sum:" << sum << std::endl;// lambda函数SumFunction = lambda_func_sum;sum = SumFunction(x, y);std::cout << "lambda_func_sum:" << sum << std::endl;// 带捕获的lambda函数int base = 10;auto lambda_func_with_capture_sum = [&base](int x, int y)->int { return x + y + base; };SumFunction = lambda_func_with_capture_sum;sum = SumFunction(x, y);std::cout << "lambda_func_with_capture_sum:" << sum << std::endl;// 仿函数ImitateAdd imitate;SumFunction = imitate;sum = SumFunction(x, y);std::cout << "imitate func:" << sum << std::endl;// 函数指针func_pointer = func_sum;SumFunction = func_pointer;sum = SumFunction(x, y);std::cout << "function pointer:" << sum << std::endl;getchar();return 0;
}

        通过上面的讲解,那我们看看std::invoke是不是这样去判断的呢?(以vs2019为蓝本),先看看源码:

//[1]函数没有参数的调用方式
template <class _Callable>
_CONSTEXPR17 auto invoke(_Callable&& _Obj) noexcept(noexcept(static_cast<_Callable&&>(_Obj)()))-> decltype(static_cast<_Callable&&>(_Obj)()) {return static_cast<_Callable&&>(_Obj)();
}//[2]除1之外的其他调用方式
template <class _Callable, class _Ty1, class... _Types2>
_CONSTEXPR17 auto invoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept(noexcept(_Invoker1<_Callable, _Ty1>::_Call(static_cast<_Callable&&>(_Obj), static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...)))-> decltype(_Invoker1<_Callable, _Ty1>::_Call(static_cast<_Callable&&>(_Obj), static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...)) {if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Functor) {return static_cast<_Callable&&>(_Obj)(static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...);} else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmf_object) {return (static_cast<_Ty1&&>(_Arg1).*_Obj)(static_cast<_Types2&&>(_Args2)...);} else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmf_refwrap) {return (_Arg1.get().*_Obj)(static_cast<_Types2&&>(_Args2)...);} else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmf_pointer) {return ((*static_cast<_Ty1&&>(_Arg1)).*_Obj)(static_cast<_Types2&&>(_Args2)...);} else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmd_object) {return static_cast<_Ty1&&>(_Arg1).*_Obj;} else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmd_refwrap) {return _Arg1.get().*_Obj;} else {static_assert(_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmd_pointer, "bug in invoke");return (*static_cast<_Ty1&&>(_Arg1)).*_Obj;}
}

从上面的代码可以看到,传入参数 _Obj 的型别判断是通过类 _Invoker1 萃取出来的,那现在来看一下_Invoker1的庐山真面目吧:

//【1】
template <class _Callable, class _Ty1, class _Removed_cvref = _Remove_cvref_t<_Callable>,bool _Is_pmf = is_member_function_pointer_v<_Removed_cvref>,bool _Is_pmd = is_member_object_pointer_v<_Removed_cvref>>
struct _Invoker1;//【2】
template <class _Callable, class _Ty1, class _Removed_cvref>
struct _Invoker1<_Callable, _Ty1, _Removed_cvref, true, false>: conditional_t<is_base_of_v<typename _Is_memfunptr<_Removed_cvref>::_Class_type, remove_reference_t<_Ty1>>,_Invoker_pmf_object,conditional_t<_Is_specialization_v<_Remove_cvref_t<_Ty1>, reference_wrapper>, _Invoker_pmf_refwrap,_Invoker_pmf_pointer>> {}; // pointer to member function//【3】
template <class _Callable, class _Ty1, class _Removed_cvref>
struct _Invoker1<_Callable, _Ty1, _Removed_cvref, false, true>: conditional_t<is_base_of_v<typename _Is_member_object_pointer<_Removed_cvref>::_Class_type, remove_reference_t<_Ty1>>,_Invoker_pmd_object,conditional_t<_Is_specialization_v<_Remove_cvref_t<_Ty1>, reference_wrapper>, _Invoker_pmd_refwrap,_Invoker_pmd_pointer>> {}; // pointer to member data//【4】
template <class _Callable, class _Ty1, class _Removed_cvref>
struct _Invoker1<_Callable, _Ty1, _Removed_cvref, false, false> : _Invoker_functor {};

1)在【1】处通过 is_member_function_pointer_v 判断是类成员函数指针,通过 is_member_object_pointer_v 判断是类成员变量

2)在【2】处指示的的是类成员函数指针,判断参数_Arg1是否为reference_wrapper类型的,即是传入对象添加了std::ref或std::cref包装。

3)在【3】处指示的是类成员变量指针,判断参数_Arg1是否为reference_wrapper类型的,即是传入对象添加了std::ref或std::cref包装。

4)在【4】处指示的是除【2】,【3】之外的函数。

型别推导出的类型有:

enum class _Invoker_strategy {_Functor,   //普通函数,仿函数,lamdba表达式, std::function等_Pmf_object, //类成员函数,传递的是对象_Pmf_refwrap, //类成员函数,传递的是用std::ref或std::cref包装了的对象_Pmf_pointer, //类成员函数,传递的是对象的指针_Pmd_object,  //类成员变量,传递的是对象_Pmd_refwrap, //类成员变量,传递的是用std::ref或std::cref包装了的对象_Pmd_pointer  //类成员变量,传递的是对象的指针
};

至此,std::invoke的实现原理很清晰了吧。

4.总结

        std::invoke用起来是十分的方便,方便的背后是系统帮你做了很多影藏的东西。也同样看出,C++的模版是多么的强大。如果喜欢就快去使用吧!

        喜欢的同学点赞收藏呗!

参考:std::invoke, std::invoke_r - cppreference.com

这篇关于C++17之std::invoke: 使用和原理探究(全)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/764174

相关文章

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

Python内置函数之classmethod函数使用详解

《Python内置函数之classmethod函数使用详解》:本文主要介绍Python内置函数之classmethod函数使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 类方法定义与基本语法2. 类方法 vs 实例方法 vs 静态方法3. 核心特性与用法(1编程客

Linux中压缩、网络传输与系统监控工具的使用完整指南

《Linux中压缩、网络传输与系统监控工具的使用完整指南》在Linux系统管理中,压缩与传输工具是数据备份和远程协作的桥梁,而系统监控工具则是保障服务器稳定运行的眼睛,下面小编就来和大家详细介绍一下它... 目录引言一、压缩与解压:数据存储与传输的优化核心1. zip/unzip:通用压缩格式的便捷操作2.

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

使用Python实现可恢复式多线程下载器

《使用Python实现可恢复式多线程下载器》在数字时代,大文件下载已成为日常操作,本文将手把手教你用Python打造专业级下载器,实现断点续传,多线程加速,速度限制等功能,感兴趣的小伙伴可以了解下... 目录一、智能续传:从崩溃边缘抢救进度二、多线程加速:榨干网络带宽三、速度控制:做网络的好邻居四、终端交互

Python中注释使用方法举例详解

《Python中注释使用方法举例详解》在Python编程语言中注释是必不可少的一部分,它有助于提高代码的可读性和维护性,:本文主要介绍Python中注释使用方法的相关资料,需要的朋友可以参考下... 目录一、前言二、什么是注释?示例:三、单行注释语法:以 China编程# 开头,后面的内容为注释内容示例:示例:四

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁