前言
笔者前一段时间在学习和使用JavaScript,发现Promise
的.then
语句可以让返回结果作为下一个函数的参数进行函数的链式调用,非常实用方便,因此思考能否用C++实现类似的效果。今天与大家讨论一下笔者对此的实现。
两种实现原理
(一)通过.then
方法传入链式调用函数
使用效果如下:
function_chain([](int a, int b) { return a + b; })
.then([](int a) { return a * 2; })
.then([](int res) { std::cout << res << ' '; })
.then([] { std::cout << "Hello, World!"; })
(1, 2); // 打印6 Hello, World!
像JavaScript一样,使用一个类,包含.then
方法,使上一个函数的返回值作为下一个函数的参数进行调用。但是不同的是,笔者暂时并未实现类似JavaScript中Promise的异步处理,而是仅仅在原有线程上的链式调用。实现代码如下:
template<class>
class function_chain;
template<class R, class... Args>
class function_chain<R(Args...)> {
std::function<R(Args...)> _function_chain;
public:
template<typename T>
function_chain(T &&f) noexcept: _function_chain(std::forward<T>(f)) {}
R operator()(Args... args) const { return _function_chain(args...); }
template<typename T>
function_chain<function_return_t<T>(Args...)> then(T &&f) const {
return {[f, this](Args... args) {
if constexpr (std::is_void_v<R>) {
_function_chain(args...);
return f();
} else return f(_function_chain(args...));
}};
}
};
template<typename T>
function_chain(T) -> function_chain<function_traits_t<T>>;
然后,来解析一下这个代码的思路。基本原理是,把每次通过.then
传入的函数连接在一起成为一个函数,然后再调用。首先是采用了一个偏特化来获取函数类型的返回类型和参数类型。function_chain
包含一个私有成员_function_chain
,是储存了被连接后的函数;重载了()
运算符,通过调用()
运算符,来开启对整个函数链的调用;.then
方法用于向函数链中添加函数。构造函数和()
运算符重载的原理较为简单,在此不做阐释。
.then
方法是一个函数模板,传入一个可调用类型,经过连接处理后,返回一个新生成的function_chain
,里面是连接后的函数链。这个新生成的function_chain
,同样用lambda
构造,参数是最初的参数,返回值是新添加函数的返回值。使用function_return_t
获取参数返回值,可以参考笔者之前发过的文章C++玩转模板之——函数萃取function traits,不过你也可以使用std::result_of
或是std::invoke_result
来获取返回值类型。然后在这个lambda
中,首先要判断当前函数的返回类型是否为void
,如果是的话就要先调用当前先函数,再调用传入的函数;如果不是就直接以当前函数调用后的的结果作为传入函数的参数进行调用即可。由于这个判断是在编译期,为了不必要的分支被编译,于是我们采用if constexpr
语法进行分支判断。
[f, this](Args... args) {
if constexpr (std::is_void_v<R>) {
_function_chain(args...);
return f();
} else return f(_function_chain(args...));
};
于是,使用.then
方法来向函数链中添加新的函数已经实现了,整个function_chain
也已经实现。为了模板自动推导,再加上最后的推断指引就好了。推断指引同样采用了function_traits_t
,详见笔者之前发过的文章C++玩转模板之——函数萃取function traits。
(二)通过重载二元运算符链式调用函数
使用效果如下:
function_pipe f1 = [](int a, int b) { return a + b; };
function_pipe f2 = [](int a) { return a * 2; };
function_pipe f3 = [](int res) { std::cout << res; };
1 | f1(2) | f2 | f3; // 打印6
通过重载类的二元运算符,同样可以实现将前一个函数的返回值,作为下一个函数的参数。在此使用管道|
运算符,让链式调用更加美观。此外,通过这种实现方式,还可以支持上一个函数的返回值和下一个函数的参数不完全相同,而是仅和第一个参数相同,这样可以在链式调用的时候补充剩余的参数。实现代码如下:
template<class>
class function_pipe;
template<class R, class Arg0, class... Args>
class function_pipe<R(Arg0, Args...)> {
std::function<R(Arg0, Args...)> _func;
std::tuple<Args...> _args;
public:
template<class T>
function_pipe(T &&f) : _func(std::forward<T>(f)) {}
function_pipe<R(Arg0, Args...)> &operator()(Args... args) {
_args = {args...};
return *this;
}
friend R operator|(Arg0 arg0, const function_pipe<R(Arg0, Args...)> &pf) {
return std::apply(pf._func, std::tuple_cat(std::tuple(arg0), pf._args));
}
};
template<class T>
function_pipe(T) -> function_pipe<function_traits_t<T>>;
现在来解析一下这个代码的思路。仍然是采用了一个偏特化来获取函数类型的返回类型和参数类型,这里由于上一次调用的函数返回值需要作为下一个函数的第一个参数值,因此偏特化的时候需要额外增加一个类型参数,以获取第一个参数的类型。function_pipe
包含了私有成员_func
,储存了当前的包含的函数,以及一个元组_args
,储存了除了第一个参数以外的剩余参数。构造函数依然是传入一个可调用对象。重载了()
运算符,用于储存剩余参数,以便在传入第一个参数时调用。
重载|
运算符,这是一个二元运算符,需调用的函数的第一个参数,是这个二元运算符的第一个参数,后面是待调用的对函数包装后的function_pipe
。原理便是将传入的第一个参数,和这个function_pipe
里的剩余参数_args,组成一个新的元组,然后使用std::apply
调用里面的_func
即可。因此每两个二元运算符调用后的返回结果,成为了下一个二元运算符的第一个参数,便实现了链式调用。
通过重载二元运算符来实现函数链式调用便已实现,最后仍然是加一个推断指引。
总结
上述以两种方式实现了C++类似函数链式调用的效果。两种实现原理均涉及类型推断,需使用C++20。如果对函数萃取的原理感兴趣,请见C++玩转模板之——函数萃取function traits。
感谢支持!