深入解析外观模式:一个C++模板实现的通用外观类库
个人专著《C++元编程与通用设计模式实现》由清华大学出版社出版。该书内容源于工业级项目实践,出版后市场反馈积极(已加印)。其专业价值获得了图书馆系统的广泛认可:不仅被中国国家图书馆作为流通与保存本收藏,还被近半数省级公共图书馆及清华大学、浙江大学等超过35所高校图书馆收录为馆藏。
个人软仓,gitee搜索“galaxy_0”
深入解析外观模式:一个C++模板实现的通用外观类库
1. 外观模式简介
在软件开发中,我们经常会遇到这样的场景:一个系统由许多子模块组成,它们之间相互协作,共同完成复杂的任务。但如果客户端需要直接与每个子模块打交道,代码很快就会变得臃肿且难以维护——就像你要去政府部门办事,却需要自己跑遍每一个窗口,多麻烦啊!这时候,外观模式(Facade Pattern)就派上用场了。它就像是一个贴心的前台接待员,你只需要跟她说明需求,她就会帮你协调好背后所有的部门,让你轻松搞定。
今天,我们就来认识一个基于C++模板实现的通用外观类库。它能把多个不同类型的对象(只要它们继承自同一个接口)统一管理起来,并提供统一的调用入口。你只需要告诉它“我要做什么”,它就会自动帮你遍历所有子对象,把活儿干完。是不是很方便?让我们一起来看看它是怎么做到的吧!
2. 代码设计解析
2.1 整体设计思路
这个外观类的设计思路其实很朴素:把一堆实现了同一接口的对象存起来,然后提供一个统一的接口来操作它们。但它的实现却很巧妙,充分利用了C++模板元编程的特性,在编译期就完成了类型检查,保证了代码的安全性,同时又没有额外的运行时开销。它的几个核心亮点包括:
- 编译期类型检查:通过递归模板继承,确保每个子类型都正确继承自指定的接口,而且接口必须有虚析构函数(这样多态删除时才安全)。
- 灵活的构造方式:你可以通过原始指针列表或
std::vector来初始化它,怎么方便怎么来。 - 统一的执行入口:
run方法接受一个可调用对象(比如lambda表达式),然后把这个操作应用到每一个子对象上,参数还可以完美转发。 - 自定义删除器:内部使用带删除器的
std::shared_ptr,这样你就能自由控制对象的销毁方式——比如对于栈上分配的对象,可以用空删除器避免误删。
为了实现这些功能,它还设计了几个幕后英雄:
TYPE_CHK_HELPER__:躲在facade_private__命名空间里,负责编译期递归类型检查。INIT_HELPER__:一个递归模板结构,帮我们在构造函数里从参数包初始化内部的vector。deltor_t:一个自定义删除器,和shared_ptr配合使用,让内存管理更安全。
2.2 类型检查帮助类:TYPE_CHK_HELPER__
先来看看这个默默无闻的检查员长什么样:
namespacefacade_private__{template<typenameitfcType,typename...implType>structTYPE_CHK_HELPER__{};template<typenameitfcType,typenameimplType1,typename...implType>structTYPE_CHK_HELPER__<itfcType,implType1,implType...>:publicTYPE_CHK_HELPER__<itfcType,implType...>{static_assert(std::has_virtual_destructor<itfcType>::value,"接口类型必须有虚析构函数");static_assert(std::is_base_of<itfcType,implType1>::value,"子类型必须是接口类型的派生类");};template<typenameitfcType>structTYPE_CHK_HELPER__<itfcType>{};}它的工作方式有点像是在玩叠罗汉:
- 主模板只是一个空壳,用来匹配任意数量的类型参数。
- 偏特化版本每次剥掉第一个子类型
implType1,然后用两个静态断言来检查它:- 接口类型必须有虚析构函数(这样才能安全地用基类指针删除派生类对象)。
- 子类型必须派生自接口类型(否则怎么叫“子类型”呢?)。
- 检查完第一个,它就继承自己,去检查剩下的子类型。这样一层层递归下去,直到所有类型都被检查完毕。当没有剩余类型时,就匹配到空参数版本,递归结束。
这样一来,只要你的某个子类型不符合要求,编译器就会在模板实例化时毫不留情地报错,把问题扼杀在摇篮里。这种“递归继承”的技巧,是C++模板元编程中常见的模式,非常巧妙!
2.3 初始化助手:INIT_HELPER__
构造函数里接受的可变参数怎么塞进vector里呢?答案是用INIT_HELPER__递归展开参数包。而且它现在已经完美适配了存储shared_ptr的容器:
template<intN,typenametupleParams>structINIT_HELPER__{staticvoidinit__(std::vector<std::shared_ptr<itfc_t,deltor_t>>&vec,tupleParams&t){vec[N-1]=std::get<N-1>(t);INIT_HELPER__<N-1,tupleParams>::init__(vec,t);}};template<typenametupleParams>structINIT_HELPER__<0,tupleParams>{staticvoidinit__(std::vector<std::shared_ptr<itfc_t,deltor_t>>&,tupleParams&){}};它的工作流程就像剥洋葱:
- 先把传入的指针们打包成一个
std::tuple。 - 从
N递减到1,每次从元组里取出对应位置的指针,塞到vector的对应位置(构造函数里已经提前resize好了)。 - 当
N减到0时,任务完成,收工!
这里最妙的是赋值那一行:右边是原始指针,左边是shared_ptr,C++会自动用原始指针和自定义删除器构造出一个临时的shared_ptr,然后移动赋值给容器里的元素。因为删除器是空删除器,所以构造出来的shared_ptr不会拥有对象所有权,完美避免了误删问题。
2.4 自定义删除器:deltor_t
structdeltor_t{voidoperator()(itfc_t*){}// 空删除器,什么也不做};这个空删除器可以说是整个设计里的“定海神针”。它存在的理由很简单:外观类可能只是临时借用一下对象,并不真正拥有它们。比如对象是在栈上分配的,或者由其他地方管理。如果用了普通的shared_ptr,一旦外观对象销毁,它就会尝试delete这些指针——如果指针指向的是栈对象,那程序就直接崩溃了。空删除器就是用来避免这种悲剧的:它告诉shared_ptr,“你不用管怎么删,让我来处理”。这样,shared_ptr只会进行引用计数,但不会真的去delete,既安全又灵活。
2.5 外观主类:facade
好了,主角终于登场了!让我们看看完整的facade类长什么样:
template<typenameitfcType,typename...subTypes>classfacade{public:usingitfc_t=typenamestd::remove_pointer<typenamestd::decay<itfcType>::type>::type;usingiterator=typenamestd::vector<std::shared_ptr<itfc_t,deltor_t>>::iterator;structdeltor_t{voidoperator()(itfc_t*){}};protected:std::vector<std::shared_ptr<itfc_t>,deltor_t>m_subs__;// 子类型容器facade_private__::TYPE_CHK_HELPER__<itfc_t,subTypes...>m_chker__[0];// 类型检查触发public:facade(){}facade(std::vector<itfc_t*>subs):m_subs__(subs){}// 从原始指针vector构造// 构造函数:接受多个原始指针facade(subTypes*...subs):m_subs__(sizeof...(subTypes)){std::tuple<subTypes*...>t=std::make_tuple(subs...);INIT_HELPER__<sizeof...(subTypes),decltype(t)>::init__(m_subs__,t);}virtual~facade(){}// 添加子类型(接受带有自定义删除器的 shared_ptr)template<typenamesubType>voidadd(std::shared_ptr<subType,deltor_t>obj){facade_private__::TYPE_CHK_HELPER__<itfc_t,subType>m_chker__[0];m_subs__.push_back(std::dynamic_pointer_cast<itfc_t>(obj));}voiderase(iterator it){m_subs__.erase(it);}