跳转至

基于“模板技术的静态多态”实现扩展现有接口的参数类型与个数

原文链接:基于可变模板参数的静态多态
本文是对原文的学习笔记

引言:动态多态的问题

C++基于虚函数很容易实现动态多态,只需重写基类方法即可:

struct Base {
    virtual void read(const std::vector<char>& buf) {}
};

struct Derived : public Base {
    void read(const std::vector<char>& buf) override {}
};

但动态多态会遇到一些问题

  1. 若将来要在 现有接口上,扩展参数类型 ,只能在Base中添加一个新接口,从而违背开闭原则。同样, 扩展参数个数 也是如此,也会违背开闭原则
  2. 模板函数不能是虚函数

在现有接口上,扩展参数类型

在现有接口上,扩展参数类型时,只能为Base添加新的接口。如此,Base每次都会被修改,这不符合开闭原则(对扩展开放,对修改关闭)。

struct Base {
    //原先接口
    virtual void read(const std::vector<char>& buf) {}

    //在read接口上,新增参数类型,就要新增接口
    virtual void read(const std::string& buf) {}
    virtual void read(const std::array<char, 20>& buf) {}
}

struct Derived : public Base {
    void read(const std::vector<char>& buf) {}
    void read(const std::string& buf) {}
    void read(const std::array<char, 20>& buf) {}
}

这种方式,实际上是,在「动态多态」的基础上,只能通过「基于函数实现的静态多态」方法,实现在 现有接口扩展函数的参数类型 。这样会影响基类接口的稳定性,因而违背了“开闭原则”,不是良好的接口设计。

模板函数不能是虚函数

能否借助 模板函数 来实现 扩展参数类型 呢?

  • 编译报错:error: templates may not be 'virtual'
struct Base{
    template<typename Buffer>
    virtual void read(const Buffer& buffer) {}
}

struct Derived : public Base {
    void read(const std::vector<char>& buf) {}
    void read(const std::string& buf) {}
    void read(const std::array<char, 20>& buf) {}
};

基于模板技术的静态多态

以上所提问题,本质上是由“动态多态”+“函数静态多态”所引起的,它们会违背“开闭原则”。而,使用基于模板技术的静态多态,即可解决此类问题。

参数类型的可扩展(模板静态多态)

使用「模板技术的静态多态」,可以实现,在不修改基类接口的情况下,对现有接口自由扩展read的参数类型。

#include<iostream>
#include<array>

//基础接口
template<typename Impl>
struct Base{
    //抽象接口,没有规定Buffer的具体类型
    template<typename Buffer>
    void read(const Buffer& buf) {
        impl_.read(buf);
    }

    Impl impl_;
};

//具体实现
struct DerivedImpl {
    void read(const std::string& buf) {
        std::cout << "read string\n";
    }
    void read(const std::array<char, 20>& buf) {
        std::cout << "read array\n";
    }
};
using Derived = Base<DerivedImpl>;

int main() {
    Derived d1{};
    d1.read("hello");

    std::array<char, 20> arr{"test"};
    d1.read(arr);
    return 0;
}

参数个数的可扩展(可变参数模板)

使用「可变参数模板」,可以实现,在不修改基类接口的情况下,自由扩展read的函数参数个数。

#include<iostream>

//基础接口
template<typename Impl>
struct Base{
    //抽象接口,没有规定Buffer的具体类型
    template<typename Buffer,typename... Args> //可变参数模板
    void read(const Buffer& buf, Args... args) {
        impl_.read(buf, args...); //将可变参数展开继续传入read中,做一个转发
    }

    Impl impl_;
};

//具体实现
struct DerivedImpl {
    void read(const std::string& buf) {
        std::cout << "read string\n";
    }
    void read(const std::string& buf, int size) {
        std::cout << "read string, size "<< size << " " << buf <<"\n";
    }
};
using Derived = Base<DerivedImpl>;

int main() {
    Derived d1{};
    d1.read("hello");
    d1.read("hello", 42);
    return 0;
}

函数名亦可扩展(仿函数)

是否有一种办法,使得连函数名称也可以扩展。如此

  • 基类的稳定性大大增加,很可能永远都无需改变
  • 用户程序也可以随意对扩展,而无需重新编译基类所在的库

基于「仿函数」,可以实现,在不修改基类接口的情况下,自由扩展基类的函数名称。

  • 原文作者将此“基类接口”其称为“上帝接口”
#include <iostream>
using namespace std;

#include <array>

//抽象接口,为客户端提供一个稳定的接口
template<typename Op, typename Buffer, typename... Args>
void god_operator_interface(Op op, Buffer buf, Args... args) {
    op(buf, args...);
}

struct Derived{
    struct read_impl {
        void operator()(const std::string& buf) {
            std::cout << "[read string] " << buf << std::endl;
        }

        //函数类型可扩展
        void operator()(const std::array<char, 20>& buf) {
            std::cout << "[read array] " << buf.data() << std::endl;
        }

        //函数参数可扩展
        void operator()(const std::string& buf, int size) {
          std::cout << "[read string, size] " << buf << " " << size << std::endl;
        }
    };
    void read(const std::string& buf) {
        god_operator_interface(read_impl{}, buf);
    }
    void read(const std::array<char, 20>& buf) {
        god_operator_interface(read_impl{}, buf);
    }
    void read(const std::string& buf, int size) {
        god_operator_interface(read_impl{}, buf, size);
    }

    //函数名可扩展
    struct read2_impl {
        void operator()(const std::string& buf) {
            std::cout << "[read2 string] " << buf << std::endl;
        }
    };
    void read2(const std::string& buf) {
        god_operator_interface(read2_impl{}, buf);
    }
};

int main()
{
    Derived d1;
    d1.read("old interface");

    std::array<char, 20> arr{"Function type"};
    d1.read(arr);

    d1.read("Function parameter", 2);

    d1.read2("Function name");
    return 0;
}

如此,即可保持god_operation_interface接口不变的情况下,用户程序可扩展其函数名、参数类型、参数个数。

项目实例

  1. 在雅兰亭库coro_rpc中就大量使用了,通过它coro_rpc提供了很多扩展点,让用户可以自由的扩展支持其它rpc和序列化,有兴趣的可以看看这个例子,扩展coro_rpc让它支持rest_rpc协议
  2. 关于更复杂的god接口则在asio中大量的应用,有兴趣可以看看asio的god接口async_initiate的实现