
Partial,大家应该比较熟悉,在模板编程中的偏特化就用这个单词,有过python编程经验的更容易理解。Partial application,大家可以把它称为“偏应用”或“部分应用”。这个名字听上去有点特别,但如果有模板偏特化的知识的就可以顺理成章的明白,可能就是把一些未知的值给定义出来,确实也是如此。
在函数编程中,将一个多参函数的部分参数固定来创建一个较少参数的新函数。大家可以理解为把其中的部分参数“重新”给个“默认值”。如果从数学的角度来看,大家可以看作是对未知参数的一个逐步“消元”的过程。给大家一个python的例程来先入为主一下,这样更容易理解:
from functools import partial
#set function
deffunc(a, b, c, d):
return100*(a + b + c + d)
# call partial function
pf = partial(func, 1, 2, 3)
pf1 = partial(func,1,2)
pf2 = partial(func,1)
#call and print
print(pf(4)) # 1000
printf(pf1(3,4)) #1000
printf(pf2(2,3,4)) #1000
上面的代码通过partial接口处理有四个参数的函数func,返回一个函数指针pf,其中三个参数已经固定为1,2,3(pf1,pf2类似),这个函数指针只需接受一个参数即可。
在上面的分析说明和python的代码展示中,应该可以很明了的知道什么是Partial application。那么就要引入到C++的模板编程中,那么在模板编程中如何实现Partial application的应用呢?在变参模板函数的应用是不是也是这样一个个的固定参数然后不断的递归处理,那么它也可以改造后应用在Partial application开发中。看下面的例子:
#include<iostream>
#include<tuple>
template <typename Func, typename... PartialArgs> classPartial__ {
private:
Func f_;
std::tuple<PartialArgs...> pArgs_;
public:
Partial__(Func func, PartialArgs... args) : f_(func), pArgs_(args...) {}
//std::index_sequence_for是 C++14引入的一个模板别名,用于根据给定的类型参数包生成一个 std::index_sequence,其索引数量等于参数包中类型的个数。
//例如:std::index_sequence_for<T1, T2, T3> 等价于std::index_sequence<0, 1, 2>
template <typename... RestArgs> autooperator()(RestArgs... rArgs)const{ return partialImpl(std::index_sequence_for<PartialArgs...>{}, rArgs...); }
private:
template <size_t... Id, typename... RestArgs> autopartialImpl(std::index_sequence<Id...>, RestArgs... rArgs)const{
return f_(std::get<Id>(pArgs_)..., rArgs...);
}
};
template <typename Func, typename... PartialArgs> autopartial(Func func, PartialArgs... args){ return Partial__<Func, PartialArgs...>(func, args...); }
intmulSum(int a, int b, int c, int d){ return10 * (a + b + c + d); }
intmain(){
auto pf2 = partial(mulSum, 1, 2);
std::cout << "call pf2 result: " << pf2(3, 4) << std::endl;
auto pf3 = partial(mulSum, 1, 2, 3);
std::cout << "call pf3 result:" << pf3(4) << std::endl;
return0;
}
也可以模仿python实现使用函数指针,C++中最常用的当然是lambda表达式:
#include<iostream>
intsum(int a, int b, int c, int d){ return a + b + c + d; }
autocreateMul(int a){
return [a](int b){return a*b;};
}
intmain(){
//单参数处理
auto pSum = [](int b, int c, int d) { return sum(1, b, c, d); };
std::cout << "pSum ret: " << pSum(2, 3, 4) << std::endl;
//通用
auto partial = [](auto func, auto... partialArgs) { return [func, partialArgs...](auto... rArgs) { return func(partialArgs..., rArgs...); }; };
auto pSum1 = partial(sum, 1);
std::cout << "pSum1 ret: " << pSum1(2, 3, 4) << std::endl;
//return lambda
std::cout<<"return lambda ,ret:"<<createMul(2)(5)<<std::endl;
return0;
}
如果在C++20以上,也可以使用Lambda表达式中的模板参数包展开机制:
#include<iostream>
#include<utility>
template <typename F, typename... Args>
autopartialImplFunc(F &&f, Args &&...allArgs){
//Lambda表达式捕获列表中展开参数包
return [f = std::forward<F>(f), ... allArgs = std::forward<Args>(allArgs)](auto &&...rArgs) mutable {
return f(allArgs..., std::forward<decltype(rArgs)>(rArgs)...);
};
}
intmultiply(int a, int b, int c, int d){ return a * b * c * d; }
intmain(){
auto func = partialImplFunc(multiply, 1, 2);
std::cout << func(3, 4) << std::endl;
return0;
}
如果在C++23的环境中则可以使用this指针和std::bind_front接口:
classmultiply {
public:
template<typename M>
autooperator()(this M&& myself, int a, int b){
return a * b * myself.num_;
}
int num_ = 1;
};
voidtestCpp23(){
multiply demo{10};
auto fBind = std::bind_front(&multiply::operator(), &demo, 10);
std::cout << "fBind ret : " << fBind(10) << std::endl;
}
上面的代码在前面的文章中都有过分析,包括std::index_sequence等,对C++20中的用法也在注释中进行了说明,如果不明白,大家可参看相关的技术文档即可。
另外,在元编程的应用中,偏特化从某种角度来看也算是一种Partial application应用。另外,也可以使用std::bind函数来模拟实现功能,不过它看起来还是有些与python的实现或者说正常的实现有些不一样。
Partial application应用一个特点是惰性加载或者说延迟计算,意思就是直到函数最终的调用时才会被执行。正如上面的例子,它可以不断的变换参数的数量,自由的创建函数的变体。即可以理解为创建多个有默认参数的别名同实现函数也可以认为创建多个有重载意义的不同名函数。
这种用法在普通函数编程中,确实意义没有多大,更多还是应用于模板的元编程中。
新技术的应用,无非就是为了“偷懒”或解决现有技术无法方便解决的问题,Partial application出现也是如此。其应用的场景主要有:
下面给出一个简单的例程,用来处理不同的任务:
#include<functional>
#include<iostream>
#include<map>
#include<string>
classTaskWrap {
public:
using Task = std::function<void(TaskWrap &, TaskWrap &)>;
voidaddTask(conststd::string &id, Task task){ tasks_[id] = task; }
voidrunTask(conststd::string &id, TaskWrap &tw){
if (tasks_.find(id) != tasks_.end()) {
tasks_[id](*this, tw);
}
}
private:
std::map<std::string, Task> tasks_;
};
classTaskGenerator {
public:
// create task1
autocreateTask1(int sign, int owner){
return [sign, owner](TaskWrap &tw1, TaskWrap &tw2) { std::cout << "run task1 sign:" << sign << std::endl; };
}
// create task2
autocreateTask2(int sign, int owner){
return [sign, owner](TaskWrap &tw1, TaskWrap &tw2) { std::cout << "run task2 sign:" << sign << std::endl; };
}
// create task3
autocreateTask3(conststd::string &id, int t1, int t2){
return [id, t1, t2](TaskWrap &tw1, TaskWrap &tw2) { std::cout << "run task3 id:" << id << " and run " << t1 << " - " << t2 << std::endl; };
}
};
intmain(){
TaskGenerator tg;
TaskWrap runner;
TaskWrap worker;
auto hBrush = tg.createTask1(1, 2);
auto hWash = tg.createTask2(5, 6);
auto hDress = tg.createTask3("dress", 7, 8);
runner.addTask("brush", hBrush);
runner.addTask("wash", hWash);
runner.addTask("dress", hDress);
runner.runTask("brush", worker);
runner.runTask("wash", runner);
runner.runTask("dress", runner);
return0;
}
古人经常说“天下文章一大抄”,这开发语言之间其实也是类似。都是发现某些问题在当前语言无法方便快捷的处理的情况下又创建了一个新的语言。C++的特点就在于其高度的底层性和灵活性,可以模拟实现各种具体的高级语言的特性,这本来是它的优势,但为了实现这些特性,其过程变得相当复杂。这反而让很多开发者认为C++难于掌握,正所谓优势即劣势,这就是事物的两个方面。