初识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的实现,当然标准库的实现会更为复杂,因为其要考虑通用性和灵活性,但其基本原理本文所述一致。