C++多态库Proxy学习笔记(一)

本文是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 AnimalDog 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);
}

“代码丑到这个样子,性能一定快到飞起吧!”。