基于“模板技术的静态多态”实现扩展现有接口的参数类型与个数
原文链接:基于可变模板参数的静态多态
本文是对原文的学习笔记
引言:动态多态的问题¶
C++基于虚函数很容易实现动态多态,只需重写基类方法即可:
struct Base {
virtual void read(const std::vector<char>& buf) {}
};
struct Derived : public Base {
void read(const std::vector<char>& buf) override {}
};
但动态多态会遇到一些问题
- 若将来要在 现有接口上,扩展参数类型 ,只能在
Base
中添加一个新接口,从而违背开闭原则。同样, 扩展参数个数 也是如此,也会违背开闭原则 - 模板函数不能是虚函数
在现有接口上,扩展参数类型¶
在现有接口上,扩展参数类型时,只能为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
接口不变的情况下,用户程序可扩展其函数名、参数类型、参数个数。
项目实例¶
- 在雅兰亭库coro_rpc中就大量使用了,通过它coro_rpc提供了很多扩展点,让用户可以自由的扩展支持其它rpc和序列化,有兴趣的可以看看这个例子,扩展coro_rpc让它支持rest_rpc协议
- 关于更复杂的god接口则在asio中大量的应用,有兴趣可以看看asio的god接口async_initiate的实现