std::invoke解析

初识std::invoke

        std::invoke是c++17标准库引入的一个函数模板。这个函数模板能做什么?原理是什么?先来看一个简单的例子,回答std::invoke“能做什么”。

#include <functional>
#include <iostream>
#include <algorithm>
#include <vector>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

struct calc {
    int add(int a, int b) { return a + b; }
    int sub(int a, int b) { return a - b; }
};

int main(int argc, char **argv)
{
    int _add = std::invoke(add, 4, 5);
    int _sub = std::invoke(sub, 7, 3);
    calc cobj;
    _add = std::invoke(&calc::add, cobj, 40, 5);
    _sub = std::invoke(&calc::sub, cobj, 70, 3);
    
    return 0;
}

        通过上面的例子,可以看出,std::invoke仅仅是对函数指针的调用做了一下封装,这种技术被称作委托。单从上面的例子来看,std::invoke的存在没有任何意义:直接调用函数更加方便,效率也更高,而通过std::invoke调用并不能带来任何收益。再来看一个例子:

#include <functional>
#include <iostream>
#include <algorithm>

template <typename T>
class rectangle {
public:
    rectangle(void) {}
    
    T area_for_rectangle(T a, T b) { return a * b; }
};

template <typename T>
class circular {
public:
    circular(void) {}
    T area_for_circular(T r) { return 3.1415926 * r * r; }
};

template <typename F, typename G, typename ... Ts>
void report_area(F func, G &obj, Ts ... args) {
    auto area = std::invoke(func, obj, args ...);
    std::cout << "area for " << typeid(G).name() << " is " << area << std::endl;
}

int main(int argc, char **argv)
{
    rectangle<int> rect;
    report_area(&rectangle<int>::area_for_rectangle, rect, 15, 20);
    
    circular<double> circ;
    report_area(&circular<double>::area_for_circular, circ, 1.0);
    
    return 0;

        在这个例子中,std::invoke似乎不可取代。但仔细研究之后,发现其仍然并非不可取代,其取代方案如下:

//...
template <typename F, typename G, typename ... Ts>
void report_area(F func, G &obj, Ts ... args) {
    auto area = (obj.*func)(args ...);
    std::cout << "area for " << typeid(G).name() << " is " << area << std::endl;
}
//...

        仔细分析后会发现,std::invoke并非必须存在,其最大意义在于调用函数指针时,不必再使用复杂的指针语法,std::invoke(func, obj, args ...)总比(obj.*func)(args ...)可读性要好。 

 invoke实现

        要想理解invoke实现,需要理解函数指针,因为invoke的本质就是对函数指针调用的封装。下面是invoke的一个简单实现,虽然简单,但足以说明invoke原理。

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

typedef int (*calc_f)(int, int);
int invoke(calc_f func, int a, int b) { return func(a, b); }

struct calc {
    int add(int a, int b) { return a + b; }
    int sub(int a, int b) { return a - b; }
};

typedef int (calc::*calc_mf)(int, int);
int invoke(calc_mf func, calc &cobj, int a, int b) { return (cobj.*func)(a, b); }

int main(int argc, char **argv)
{
    int _add = invoke(add, 4, 5);
    int _sub = invoke(sub, 7, 3);
    calc cobj;
    _add = invoke(&calc::add, cobj, 40, 5);
    _sub = invoke(&calc::sub, cobj, 70, 3);
    
    return 0;
}

         上面的invoke实现通用性很差。如果要实现一个通用较好的invoke,至少需要解决三点问题:

  • 区分函数指针类型:类成员函数还是全局函数
  • 参数转发处理,参数类型和数量都不确定
  • 确定函数的返回值类型

        关于第一点,上面的例子已经通过函数重载,增加类的对象实例实现;第二点可以通过变参模板实现;第三点对于返回值类型的推导有两种方案:一使用auto,二使用decltype。

使用auto推导返回值类型

//...
//模板1
template <typename Fp, typename ... Args>
auto invoke(Fp func, Args ... args) { return func(args ...); }

//模板2
template <typename Fp, typename Tp, typename ... Args>
auto invoke(Fp func, Tp obj, Args ... args) { return (obj.*func)(args ...); }

//...

    int _add, _sub;
//实例1
    _add = invoke(add, 4, 5);
    _sub = invoke(sub, 7, 3);
//实例2    
    calc cobj;
    _add = invoke(&calc::add, cobj, 40, 5);
    _sub = invoke(&calc::sub, cobj, 70, 3);

//...

         关于上面的源码,调用全局函数和类成员函数的invoke单独编译,是没有问题的。但放到一起,编译无法通过。编译会会使用 invoke(Fp func, Tp obj, Args ... args)推导invoke(add, 4, 5),因此,会出现下面的错误:

error: right hand operand to .* has non-pointer-to-member type 'int (*)(int, int)'
auto invoke(Fp func, Tp obj, Args ... args) { return (obj.*func)(args ...); }

note: in instantiation of function template specialization 'invoke<int (*)(int, int), int, int>' requested here
    _add = invoke(add, 4, 5);

        关于这个无误,解决也比较简单,使用std::enable_if做下判断即可。新的实现如下:

//...
template <typename Fp, typename ... Args>
auto invoke(Fp func, Args ... args) { return func(args ...); }

template <typename Fp, typename Tp, typename ... Args, typename enable = typename std::enable_if<std::is_member_function_pointer<Fp>::value>::type>
auto invoke(Fp func, Tp obj, Args ... args) { return (obj.*func)(args ...); }
//...

使用decltype推导返回值类型 

         返回值类型使用auto进行推导,至少要到C++14才会支持。对于C++11,返回值类型也可以使用decltype关键字进行推断,源码如下:

//...
template <typename Fp, typename ... Args>
decltype(std::declval<Fp>()(std::declval<Args>()...)) invoke(Fp func, Args ... args) { return func(args ...); }

template <typename Fp, typename Tp, typename ... Args>
decltype((std::declval<Tp>().*std::declval<Fp>())(std::declval<Args>()...)) invoke(Fp func, Tp obj, Args ... args) { return (obj.*func)(args ...); }
//...

        使用decltype进行返回值类型推断,需要用到std::declval工具。因为对类成员函数的调用,需要定义类对象,而std::declval使在没有类对象的情况下调用类成员函数称为一种可能。如果不涉及到类的成员函数,完全可以不适用std::cval工具。源码可以简化实现,如下:

//...
template <typename Fp, typename ... Args>
decltype(Fp()(Args()...)) invoke(Fp func, Args ... args) { return func(args ...); }

template <typename Fp, typename Tp, typename ... Args>
decltype((std::declval<Tp>().*std::declval<Fp>())(Args()...)) invoke(Fp func, Tp obj, Args ... args) { return (obj.*func)(args ...); }
//...

        使用decltype与auto比较,尽管代码有失简洁,但使用decltype可以不必在使用std::enable_if进行推导判断。

        这便是invoke的实现,当然标准库的实现会更为复杂,因为其要考虑通用性和灵活性,但其基本原理本文所述一致。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值