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

目录

1.概述

2.辅助类

3.原理分析

4.总结


1.概述

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

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);
 
#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
}

可能的输出:

cpp 复制代码
-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.函数指针。和数组名一样,函数名即为函数指针。如:

cpp 复制代码
	typedef void(*FType)(int); //定义一个函数指针类型Ftype
	void func(FType fn, int x) {
		fn(x);
	}

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

cpp 复制代码
	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()运算符的类对象,如:

cpp 复制代码
    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(),也是运用了仿函数的思想,如:

cpp 复制代码
#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表达式,如:

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

6.std::function, 如:

cpp 复制代码
#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为蓝本),先看看源码:

cpp 复制代码
//[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的庐山真面目吧:

cpp 复制代码
//【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】之外的函数。

型别推导出的类型有:

cpp 复制代码
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

相关推荐
天下皆白_唯我独黑7 分钟前
php 使用qrcode制作二维码图片
开发语言·php
夜雨翦春韭10 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
小远yyds12 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
何曾参静谧24 分钟前
「C/C++」C/C++ 之 变量作用域详解
c语言·开发语言·c++
AI街潜水的八角34 分钟前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习
q567315231 小时前
在 Bash 中获取 Python 模块变量列
开发语言·python·bash
JSU_曾是此间年少1 小时前
数据结构——线性表与链表
数据结构·c++·算法
许野平1 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
也无晴也无风雨1 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
狂奔solar1 小时前
yelp数据集上识别潜在的热门商家
开发语言·python