本文是C++20标准下微软提供实现多态能力的头文件库的学习笔记。当前网络上虽然有很多相关文章了,但是本文侧重点除了学习库怎么用以外,还深入探讨Proxy库用到的C++14、C++17、C++20等的特性,学习Proxy是怎么实现的。
常规C++的多态实现
实现容器基类,然后派生动态数组和集合类型:
class Container{
public:
virtual void insert(...);
virtual void remove(...);
virtual ~Container();
}
class Vector:Container{
public:
virtual void insert(...) override;
virtual void remove(...) override;
virtual ~Vector();
}
class Set:Container{
public:
virtual void insert(...) override;
virtual void remove(...) override;
virtual ~Set();
}
在使用时,我们可以就针对Container操作就可以了。
void do_some_thing(Container* c){
c->insert(...);
if(...){
c->remove(...);
}
}
int main(){
Vector v;
Set s;
do_smoe_thing(&v);
do_smoe_thing(&s);
return 0;
}
CPP这个是很简单的例子。之前我还长时间维护过芯片行业的电路仿真引擎,它最早是上个世纪七八十年代伯克利大学设计的C语言项目。项目巨大,维护复杂,在一众面向对象语言出现前,面向对象和多态的思想已经在C语言中开始尝试了。它是这么处理多态的:
struct Container{
void (*insert_ptr)(...);
void (*remove_ptr)(...);
}
struct Vector{
void (*insert_ptr)(...);
void (*remove_ptr)(...);
void insert(...);
void remove(...);
void init(){
insert_ptr = insert;
remove_ptr = remove;
}
}
struct Set{
Container c;
void insert(...);
void remove(...);
void init(){
c.insert_ptr = insert;
c.remove_ptr = remove;
}
}
void do_some_thing(Container* c){
c->insert(...);
if(...){
c->remove(...)
}
}
int main(){
Vector v;
v.init();
Set s;
s.init();
do_some_thing((Container*)(&v));
do_some_thing(&s.c);
return 0;
}
上面在实现Vector和Set的时候分别提供了两种写法,原因是在上述引擎代码中都有体现,它通过C语言明确的内存布局,利用强制类型转换和函数指针实现多态能力。当然这么实现太灵活、太危险,稍稍大意就会写错漏写,或者成员函数顺序错误。
CPP模板
上面东拉西扯一些入门知识,主要是铺垫多态实现的思路,现在很多人发出了思想挑战:多态不一定要通过虚函数、虚表来实现。比如CPP的模板编程也可以:
template<class Container>
void do_something(Container& c){
c.insert(...);
if(...){
c.remove(...);
}
}
#include<vector>
#include<set>
int main(){
std::vector<int> v;
std::set<int> s;
do_something(v);
do_something(s);
return 0;
}
上面代码编译无法通过,主要是表达思路,编译器生成多个重载函数,给两次调用实现了两种不同的操作,静态的实现了多态逻辑。但是这个还是不够的,很多时候我们需要多态不仅仅是静态的,还需要动态的,它们还要是统一的类型,要能放到一个数组中的。模板编程也不是不行:
#include <functional>
struct ContainerFacade{
std::function<void(...)> insert;
std::function<void(...)> remove;
}
void do_something(std::vector<ContainerFacade> containers){
for(auto c : containers){
c.insert(...);
if(...){
c.remove(...);
}
}
}
#include <vector>
#include <set>
int main(){
std::vector<int> v;
std::set<int> s;
do_something({ContainerFacade{std::bind(&std::vector<int>::emplace_back,
&v, std::placeholder::_1),
std::bind(&std::vector<int>::remove,
&v, std::placeholder::_1)},
ContainerFacade{std::bind(&std::set<int>::insert,
&v, std::placeholder::_1),
std::bind(&std::set<int>::remove,
&v, std::placeholder::_1)}});
return 0;
}
利用CPP现代语言特性和语法糖,std::funcation和std::bind动态绑定,实现运行时多态。诚如网友的评价: “代码丑到这个样子,性能一定快到飞起吧!”。
其实这个实现跟第一节第二个例子,C语言的多态实现是一个思路。我们要的多态,不一定就是个继承来的对象,而是方便的统一的抽象,可以放到一起,调用相同逻辑或类似逻辑的具体函数就行。C语言的实现太过灵活,抹除了类型型别,在实践过程中对使用者的要求太过苛刻。传统的继承方法常常讨论is a
关系,Cat is a Animal
,Dog is a Animal
极为繁琐不说,在工程实践中,某个类通常是很多基类的子类。CPP可以多继承,甚至处理菱形继承还搞出了虚继承。其他语言则很多直接禁止了多继承,再构造出接口的概念,最终实现的还是多继承。使用时dynamic cast
类型转换漫天飞不说,如果已经设计好的库的类型不满足新的接口逻辑,那么还要再通过派生和继承新接口逻辑来调整,最后产生更多的隐晦的类型。
既然如此,那为什么不放弃继承关系,回到原始需求,类型只要具备特定接口的能力,能被统一的方法调用,是不是就好了?
非侵入式的运行时多态,外观和接口只是特定几个类型中的某几个函数的聚合。
上述CPP模板的案例符合这些特征,但是过程太过复杂繁琐,微软的Proxy库则提供了相对简单的实现。
Proxy
#include <proxy/proxy.h>
struct insert : pro::dispatch<void(...)>{
template <class T>
void operator()(T& self, ...){self.insert(...);}
};
struct ContainerFacade : pro::facade<insert>{};
void do_something(pro::proxy<ContainerFacade> c){
c.invoke<insert>(...);
}
#include <set>
int main(){
std::vector<int> v;
do_something(pro::make_proxy<ContainerFacade>(v));
}
当然后面还有简化版,通过宏来代替上面一长串外观和仿函数定义,调用处也简单一点,不用那个invoke了:
#include <proxy/proxy.h>
namespace spec {
PRO_DEF_MEMBER_DISPATCH(insert, void(...));
PRO_DEF_FACADE(ContainerFacade, insert);
}
void do_something(pro::proxy<spec::ContainerFacade> c){
c.insert(...);
}
#include <set>
int main(){
std::set<int> v;
do_something(&v);
}
“代码丑到这个样子,性能一定快到飞起吧!”。