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


    C++进阶专栏:http://t.csdnimg.cn/5mV9r

    目录

    1.概述

    2.辅助类

    3.原理分析

    4.总结


    1.概述

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

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

            std::invoke 的语法如下:

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

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

    1. #include
    2. #include
    3. #include
    4. struct Foo
    5. {
    6. Foo(int num) : num_(num) {}
    7. void print_add(int i) const { std::cout << num_ + i << '\n'; }
    8. int num_;
    9. };
    10. void print_num(int i)
    11. {
    12. std::cout << i << '\n';
    13. }
    14. struct PrintNum
    15. {
    16. void operator()(int i) const
    17. {
    18. std::cout << i << '\n';
    19. }
    20. };
    21. int main()
    22. {
    23. // 调用自由函数
    24. std::invoke(print_num, -9);
    25. // 调用 lambda
    26. std::invoke([]() { print_num(42); });
    27. // 调用成员函数
    28. const Foo foo(314159);
    29. std::invoke(&Foo::print_add, foo, 1);
    30. // 调用(访问)数据成员
    31. std::cout << "num_:" << std::invoke(&Foo::num_, foo) << '\n';
    32. // 调用函数对象
    33. std::invoke(PrintNum(), 18);
    34. #if defined(__cpp_lib_invoke_r)
    35. auto add = [](int x, int y) { return x + y; };
    36. auto ret = std::invoke_r<float>(add, 11, 22);
    37. static_assert(std::is_same<decltype(ret), float>());
    38. std::cout << ret << '\n';
    39. std::invoke_r<void>(print_num, 44);
    40. #endif
    41. }

    可能的输出:

    1. -9
    2. 42
    3. 314160
    4. num_:314159
    5. 18
    6. 33
    7. 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.函数指针。和数组名一样,函数名即为函数指针。如:

    1. typedef void(*FType)(int); //定义一个函数指针类型Ftype
    2. void func(FType fn, int x) {
    3. fn(x);
    4. }

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

    1. class CTestabcd
    2. {
    3. public:
    4. inline int func(int a, int b) { return a + b; }
    5. public:
    6. int m_i;
    7. };
    8. using TestFunc = int (CTestabcd::*)(int, int);
    9. using TestMember = int(CTestabcd::*);
    10. TestFunc gTestFunc = &CTestabcd::func;
    11. TestMember gTestMember = &CTestabcd::m_i;

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

    1. template <class _Ty = void>
    2. struct less {
    3. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty _FIRST_ARGUMENT_TYPE_NAME;
    4. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty _SECOND_ARGUMENT_TYPE_NAME;
    5. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool _RESULT_TYPE_NAME;
    6. _NODISCARD constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const {
    7. return _Left < _Right;
    8. }
    9. };

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

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

            5.lambda表达式,如:

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

            6.std::function, 如:

    1. #include
    2. #include
    3. // std::function
    4. std::function<int(int, int)> SumFunction;
    5. // 普通函数
    6. int func_sum(int a, int b)
    7. {
    8. return a + b;
    9. }
    10. class Calcu
    11. {
    12. public:
    13. int base = 20;
    14. // 类的成员方法,参数包含this指针
    15. int class_func_sum(const int a, const int b) const { return this->base + a + b; };
    16. // 类的静态成员方法,不包含this指针
    17. static int class_static_func_sum(const int a, const int b) { return a + b; };
    18. };
    19. // 仿函数
    20. class ImitateAdd
    21. {
    22. public:
    23. int operator()(const int a, const int b) const { return a + b; };
    24. };
    25. // lambda函数
    26. auto lambda_func_sum = [](int a, int b) -> int { return a + b; };
    27. // 函数指针
    28. int (*func_pointer)(int, int);
    29. int main(void)
    30. {
    31. int x = 2;
    32. int y = 5;
    33. // 普通函数
    34. SumFunction = func_sum;
    35. int sum = SumFunction(x, y);
    36. std::cout << "func_sum:" << sum << std::endl;
    37. // 类成员函数
    38. Calcu obj;
    39. SumFunction = std::bind(&Calcu::class_func_sum, obj,
    40. std::placeholders::_1, std::placeholders::_2); // 绑定this对象
    41. sum = SumFunction(x, y);
    42. std::cout << "Calcu::class_func_sum:" << sum << std::endl;
    43. // 类静态函数
    44. SumFunction = Calcu::class_static_func_sum;
    45. sum = SumFunction(x, y);
    46. std::cout << "Calcu::class_static_func_sum:" << sum << std::endl;
    47. // lambda函数
    48. SumFunction = lambda_func_sum;
    49. sum = SumFunction(x, y);
    50. std::cout << "lambda_func_sum:" << sum << std::endl;
    51. // 带捕获的lambda函数
    52. int base = 10;
    53. auto lambda_func_with_capture_sum = [&base](int x, int y)->int { return x + y + base; };
    54. SumFunction = lambda_func_with_capture_sum;
    55. sum = SumFunction(x, y);
    56. std::cout << "lambda_func_with_capture_sum:" << sum << std::endl;
    57. // 仿函数
    58. ImitateAdd imitate;
    59. SumFunction = imitate;
    60. sum = SumFunction(x, y);
    61. std::cout << "imitate func:" << sum << std::endl;
    62. // 函数指针
    63. func_pointer = func_sum;
    64. SumFunction = func_pointer;
    65. sum = SumFunction(x, y);
    66. std::cout << "function pointer:" << sum << std::endl;
    67. getchar();
    68. return 0;
    69. }

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

    1. //[1]函数没有参数的调用方式
    2. template <class _Callable>
    3. _CONSTEXPR17 auto invoke(_Callable&& _Obj) noexcept(noexcept(static_cast<_Callable&&>(_Obj)()))
    4. -> decltype(static_cast<_Callable&&>(_Obj)()) {
    5. return static_cast<_Callable&&>(_Obj)();
    6. }
    7. //[2]除1之外的其他调用方式
    8. template <class _Callable, class _Ty1, class... _Types2>
    9. _CONSTEXPR17 auto invoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept(
    10. noexcept(_Invoker1<_Callable, _Ty1>::_Call(
    11. static_cast<_Callable&&>(_Obj), static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...)))
    12. -> decltype(_Invoker1<_Callable, _Ty1>::_Call(
    13. static_cast<_Callable&&>(_Obj), static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...)) {
    14. if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Functor) {
    15. return static_cast<_Callable&&>(_Obj)(static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...);
    16. } else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmf_object) {
    17. return (static_cast<_Ty1&&>(_Arg1).*_Obj)(static_cast<_Types2&&>(_Args2)...);
    18. } else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmf_refwrap) {
    19. return (_Arg1.get().*_Obj)(static_cast<_Types2&&>(_Args2)...);
    20. } else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmf_pointer) {
    21. return ((*static_cast<_Ty1&&>(_Arg1)).*_Obj)(static_cast<_Types2&&>(_Args2)...);
    22. } else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmd_object) {
    23. return static_cast<_Ty1&&>(_Arg1).*_Obj;
    24. } else if constexpr (_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmd_refwrap) {
    25. return _Arg1.get().*_Obj;
    26. } else {
    27. static_assert(_Invoker1<_Callable, _Ty1>::_Strategy == _Invoker_strategy::_Pmd_pointer, "bug in invoke");
    28. return (*static_cast<_Ty1&&>(_Arg1)).*_Obj;
    29. }
    30. }

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

    1. //【1】
    2. template <class _Callable, class _Ty1, class _Removed_cvref = _Remove_cvref_t<_Callable>,
    3. bool _Is_pmf = is_member_function_pointer_v<_Removed_cvref>,
    4. bool _Is_pmd = is_member_object_pointer_v<_Removed_cvref>>
    5. struct _Invoker1;
    6. //【2】
    7. template <class _Callable, class _Ty1, class _Removed_cvref>
    8. struct _Invoker1<_Callable, _Ty1, _Removed_cvref, true, false>
    9. : conditional_ttypename _Is_memfunptr<_Removed_cvref>::_Class_type, remove_reference_t<_Ty1>>,
    10. _Invoker_pmf_object,
    11. conditional_t<_Is_specialization_v<_Remove_cvref_t<_Ty1>, reference_wrapper>, _Invoker_pmf_refwrap,
    12. _Invoker_pmf_pointer>> {}; // pointer to member function
    13. //【3】
    14. template <class _Callable, class _Ty1, class _Removed_cvref>
    15. struct _Invoker1<_Callable, _Ty1, _Removed_cvref, false, true>
    16. : conditional_t<
    17. is_base_of_v<typename _Is_member_object_pointer<_Removed_cvref>::_Class_type, remove_reference_t<_Ty1>>,
    18. _Invoker_pmd_object,
    19. conditional_t<_Is_specialization_v<_Remove_cvref_t<_Ty1>, reference_wrapper>, _Invoker_pmd_refwrap,
    20. _Invoker_pmd_pointer>> {}; // pointer to member data
    21. //【4】
    22. template <class _Callable, class _Ty1, class _Removed_cvref>
    23. 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】之外的函数。

    型别推导出的类型有:

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

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

    4.总结

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

            喜欢的同学点赞收藏呗!

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

  • 相关阅读:
    Ubuntu Anaconda 环境下删除 protobuf
    Java笔记(10)
    前端缓存机制——强缓存、弱缓存、启发式缓存
    字符串的匹配——KMP算法的学习
    uniapp 微信小程序 vue3.0+TS手写自定义封装步骤条(setup)
    Clear Code for Minimal API
    Java学习--JDBC
    Unity面试题随笔(一)
    基于SSH开发家庭收支管理系统 课程设计 大作业 毕业设计
    vue项目类微信聊天页面,输入法弹出,ios的标题会整体上移问题
  • 原文地址:https://blog.csdn.net/haokan123456789/article/details/136389418