C++设计模式之模板方法模式(TemplateMethod)
前言模板方法模式Template Method Pattern是 GoF 二十三种设计模式中的行为型模式之一。它的核心思想只有一句话在一个非虚函数里把算法的骨架固定下来把其中会变化的步骤声明为虚函数交给子类去实现。基类控制「先做什么、后做什么、什么条件下做」子类只负责「每一步具体怎么做」。这个模式在 C 里有一个绕不开的命名混淆它和 C 语言里的「模板template」没有关系。模板方法是运行期多态的产物靠虚函数和继承实现C 的template是编译期泛型机制两者可以配合使用但不是一回事。中文资料里常把前者写成「模板方法模式」后者写成「模板/泛型编程」读的时候要看清语境。另一个常见误解是把「模板方法」理解成「一个被反复调用的普通函数」。它之所以叫模板是因为算法流程像一张固定模板步骤的数量和顺序由基类写死子类只能填空不能改结构。如果子类可以整体替换流程那只是普通的虚函数重写约束就消失了。本文讲清模式的结构、C 的落地写法包括非虚接口惯用法、与策略模式的区别以及常见的实现错误。代码以 C17 为基准未经过编译运行行为按标准语义推演。一、结构固定的骨架与可变的步骤模式里有两类参与者。抽象基类定义模板方法它通常是非虚的、公开的模板方法依次调用若干「基本操作」这些基本操作是受保护的虚函数可以是纯虚必须实现也可以带默认实现称为钩子hook。具体子类重写基本操作但不重写模板方法。一个典型的调用链是client - Base::templateMethod() 非虚固定顺序- step1() 虚子类实现- step2() 虚子类实现- hook() 虚有默认实现子类可选覆盖这种分工带来两个好处。第一算法流程只有一份修改流程只需改基类不会出现「五个子类各自抄了一遍流程、改的时候漏了一个」的情况。第二子类被限制在步骤级别无法破坏步骤之间的不变式例如「必须先打开再写入」「写完必须关闭」。反过来它也有代价继承是强耦合子类必须接受基类定义的全部步骤运行期多态有虚调用开销如果步骤划分得不合理子类会被迫实现一些自己根本不关心的空函数。步骤粒度是这个模式设计时最需要斟酌的地方。二、C 落地非虚接口与访问控制C 实现模板方法时有几个语言层面的细节决定了它是否安全。第一模板方法不要声明为virtual。一旦它是虚函数子类就可以整体重写骨架的约束形同虚设。常见写法是公开的非虚函数甚至可以加final之外的修饰非虚函数本来就不能被重写但可以被子类用同名函数隐藏见坑点。第二析构函数必须是virtual。只要类打算通过基类指针delete派生对象基类析构就必须是虚的否则是未定义行为undefined behavior, UB。这一点与模板方法本身无关但只要用了继承多态就必须遵守。第三基本操作放在protected而不是public。外部调用方只应该看到模板方法这一个入口把步骤暴露为公有等于允许调用方绕过骨架直接调用某一步顺序约束就没了。更严格的做法是「非虚接口non-virtual interface, NVI」公有非虚函数做入口基本操作声明为private virtual由基类的非虚函数调用。派生类可以重写私有虚函数但外部包括其他派生类无法直接调用它。第四绝不能在构造或析构函数里调用虚函数并指望派生出效果。构造基类子对象时对象的动态类型还是基类虚调用只会调到基类版本若调用的是纯虚函数行为未定义。析构阶段同理。需要子类参与的初始化应该提供一个显式的init()或build()步骤由派生类构造完成后调用。下面是一个完整的报表导出示例流程固定为「写头 → 写正文 → 可选写脚注 → 写结束标记」具体格式由子类决定。// 适用于 C17#include iostream#include stringclass ReportGenerator {public:virtual ~ReportGenerator() default;// 模板方法非虚固定算法骨架void generate() const {write_header();write_body();if (want_footer()) {write_footer();}write_end();}protected:virtual void write_header() const 0;virtual void write_body() const 0;virtual void write_footer() const 0;// 钩子有默认实现子类可选择覆盖virtual bool want_footer() const { return true; }private:// 私有步骤子类无法覆盖也无法从外部调用void write_end() const {std::cout end \n;}};class PlainTextReport : public ReportGenerator {protected:void write_header() const override {std::cout [plain]\n;}void write_body() const override {std::cout line 1\nline 2\n;}void write_footer() const override {std::cout (no footer)\n;}bool want_footer() const override {return false; // 不输出脚注}};class CsvReport : public ReportGenerator {protected:void write_header() const override {std::cout id,name\n;}void write_body() const override {std::cout 1,alpha\n2,beta\n;}void write_footer() const override {std::cout rows2\n;}// 不覆盖 want_footer使用基类默认的 true};void export_report(const ReportGenerator r) {r.generate(); // 只依赖基类接口}int main() {PlainTextReport text;CsvReport csv;export_report(text);export_report(csv);return 0;}这段代码里generate()的调用顺序由基类唯一决定PlainTextReport通过覆盖want_footer()跳过脚注CsvReport直接继承默认钩子。write_end()是私有非虚函数子类既不能改也不能调保证结束标记一定按基类的方式输出。三、与策略模式的区别以及静态版本模板方法模式和策略模式经常被拿来比较因为两者都在「让变化的部分可替换」。区别在于变化的时机和绑定方式维度模板方法模式策略模式变化单元算法中的若干步骤整个算法绑定方式继承 虚函数运行期组合 接口对象运行期关系基类与子类是 is-a上下文与策略是 has-a扩展方式新增子类新增策略类无需改上下文复用骨架由基类复用算法可独立复用、可动态切换耦合度高子类依赖基类步骤划分低只依赖策略接口如果需求是「流程固定、个别步骤不同」模板方法合适如果需求是「整个算法可以整体替换甚至运行期切换」策略模式更合适。实践中两者可以组合模板方法里的某个步骤内部持有一个策略对象。C 还有第三种选择用奇异递归模板模式curiously recurring template pattern, CRTP把模板方法做成编译期多态// 适用于 C17template typename Derivedclass StaticReportGenerator {public:void generate() const {self().write_header();self().write_body();self().write_end();}private:const Derived self() const {return static_castconst Derived(*this);}};class PlainReport : public StaticReportGeneratorPlainReport {public:void write_header() const { /* ... */ }void write_body() const { /* ... */ }void write_end() const { /* ... */ }};CRTP 版本没有虚函数表调用可以在编译期内联代价是类型必须在编译期确定无法通过基类引用持有不同子类也不能放进同一个容器里做运行期分派。它适合性能敏感、类型集合固定的场景不属于 GoF 的原始模式但常被当作「静态模板方法」使用。常见坑点❌ 把generate()声明为virtual让子类可以整体重写。✅ 模板方法保持非虚只把步骤声明为虚函数。❌ 基类析构函数不写virtual却通过基类指针delete派生对象。✅ 写virtual ~ReportGenerator() default;否则是 UB。❌ 在基类构造函数里调用write_header()这种纯虚函数。✅ 构造期间动态类型是基类纯虚调用是 UB把初始化拆成显式的init()步骤。❌ 把write_header()等步骤声明为public。✅ 放protected或采用 NVI公有非虚入口 私有虚步骤。❌ 派生类重写步骤时漏写override签名差一个const就变成新函数。✅ 所有重写都写override让编译器检查签名。❌ 每个子类各自复制一遍generate()的流程再各自加步骤。✅ 流程只留在基类差异用钩子表达而不是复制骨架。❌ 把「模板方法模式」和 C 的template混为一谈用模板参数代替虚函数。✅ 前者是运行期多态后者是编译期泛型按是否需要运行期替换来选择。❌ 钩子的默认实现返回一个「看似安全」的值却没有文档说明。✅ 钩子要有明确语义和默认行为说明例如want_footer()默认true。总结角色职责C 落点抽象基类定义模板方法与步骤接口公有非虚generate()受保护虚步骤模板方法固定步骤顺序与条件非虚禁止子类重写基本操作子类必须实现的步骤纯虚函数钩子子类可选覆盖的扩展点带默认实现的虚函数具体子类实现步骤不碰流程overridevirtual析构模板方法模式的本质是「用继承固定流程、用虚函数开放步骤」。写的时候盯住三件事模板方法非虚、析构虚、步骤受保护。若发现子类需要改流程而不是填步骤说明模式选错了应该考虑策略模式或重新划分职责。