Template Method Pattern
Define an algorithm's invariant skeleton in a non-virtual base-class method and defer variable steps to subclasses via NVI virtual hooks or CRTP for zero-overhead static dispatch.
Template Method Patternsince C++98A behavioral design pattern that fixes an algorithm's invariant skeleton in a non-virtual base-class method and exposes variable steps as virtual hooks that subclasses override — enforcing ordering, preconditions, and postconditions at the base level.
Overview
The Template Method pattern separates what an algorithm does from how individual steps are performed. The base class owns the structure — ordering, invariants, error handling — while subclasses supply only the variable steps. Derived classes cannot reorder or skip steps they don't own.
Do not confuse this with C++ templates. The pattern predates the language feature and is named after the concept of a skeleton with blanks to fill in. CRTP uses C++ templates to implement a compile-time variant, but the pattern itself requires no template machinery.
Two implementation strategies:
| Strategy | Dispatch | Overhead | Heterogeneous collections? |
|---|---|---|---|
| Virtual (NVI) | Dynamic | vtable call | Yes, via base pointer |
| CRTP | Static | Zero (fully inlined) | No — each instantiation is a distinct type |
Choose virtual when the concrete type is unknown at compile time. Choose CRTP when the type is always fixed at compile time and vtable overhead is unacceptable (inner loops, embedded systems, high-frequency paths).
Syntax
NVI: Non-Virtual Interface
The idiomatic C++ form declares the template method public and non-virtual, with hooks in private virtual slots. Private virtuals are legal and overridable in derived classes — they just cannot be called directly by those classes, which prevents subclasses from bypassing the skeleton's invariants.
class ReportGenerator {
public:
// Template method: skeleton is invariant — cannot be overridden
void generate() {
validate_context(); // base enforces this always runs first
write_header(); // hook
write_body(); // hook — pure, subclass must provide
write_footer(); // hook
}
virtual ~ReportGenerator() = default; // C++11
private:
void validate_context() {
if (!context_valid_) throw std::logic_error{"missing context"};
}
virtual void write_header() { /* default: emit document title */ }
virtual void write_body() = 0; // required hook
virtual void write_footer() {} // optional hook — default: no-op
bool context_valid_ = true;
};
class SalesReport : public ReportGenerator {
private:
void write_body() override { // C++11 override
// emit sales data
}
void write_footer() override {
// append signature block
}
};Use protected instead of private only when derived classes legitimately need to call the hook themselves (e.g., a subclass of a subclass extending behavior via Base::write_footer()).
CRTP: Static Template Method
template<typename Derived>
class Encoder {
public:
// Template method: resolved entirely at compile time
void encode(std::span<const std::byte> input) { // std::span: C++20
auto prepared = self().prepare(input);
auto encoded = self().do_encode(prepared);
self().flush(encoded);
}
protected:
// Optional hooks — override selectively in Derived
std::vector<std::byte> prepare(std::span<const std::byte> in) {
return {in.begin(), in.end()};
}
void flush(const std::vector<std::byte>&) {}
private:
Derived& self() { return static_cast<Derived&>(*this); }
};
class Base64Encoder : public Encoder<Base64Encoder> {
friend class Encoder<Base64Encoder>;
protected:
std::vector<std::byte> do_encode(std::vector<std::byte> data) {
// ... base64 transform
return data;
}
};No vtable is generated. The compiler sees through the static_cast and inlines each hook directly, producing code identical to a sequence of direct function calls.
Examples
Data Pipeline with Enforced Invariants
#include <vector>
#include <algorithm>
#include <cmath>
#include <stdexcept>
class DataPipeline {
public:
std::vector<double> run(std::vector<double> input) {
if (input.empty())
throw std::invalid_argument{"empty input"};
auto cleaned = clean(std::move(input));
if (!validate(cleaned))
throw std::runtime_error{"post-clean validation failed"};
return transform(std::move(cleaned)); // C++11 move semantics
}
virtual ~DataPipeline() = default;
private:
virtual std::vector<double> clean(std::vector<double> v) const {
// default: strip NaN values
std::erase_if(v, [](double x) { return std::isnan(x); }); // C++20
return v;
}
virtual bool validate(const std::vector<double>& v) const {
return !v.empty();
}
virtual std::vector<double> transform(std::vector<double> v) const = 0;
};
class NormalizePipeline : public DataPipeline {
private:
std::vector<double> transform(std::vector<double> v) const override {
auto [it_min, it_max] = std::minmax_element(v.begin(), v.end()); // C++17 structured bindings
double range = *it_max - *it_min;
if (range == 0.0) return std::vector<double>(v.size(), 0.0);
for (auto& x : v) x = (x - *it_min) / range;
return v;
}
};The base class guarantees validate() always runs after clean() and that the empty-input check precedes any hook. NormalizePipeline cannot bypass or reorder those steps.
Game Application Loop
class GameApplication {
public:
void run() {
on_init();
while (!quit_) {
on_input();
on_update(target_dt_);
on_render();
}
on_shutdown();
}
virtual ~GameApplication() = default;
protected:
bool quit_ = false;
private:
static constexpr float target_dt_ = 1.0f / 60.0f; // C++11 constexpr
virtual void on_init() {}
virtual void on_input() {}
virtual void on_update(float dt) = 0;
virtual void on_render() = 0;
virtual void on_shutdown() {}
};
class SpaceGame : public GameApplication {
float ship_x_ = 0.0f;
void on_update(float dt) override {
ship_x_ += 200.0f * dt;
if (ship_x_ > 1920.0f) quit_ = true;
}
void on_render() override {
// submit GPU draw calls for ship at ship_x_
}
};Best Practices
Prefer private hooks over protected hooks. Private virtual methods are overridable but not callable by derived classes. This enforces that subclasses customize steps without being able to invoke them out of sequence. Promote to protected only when a subclass hierarchy legitimately chains calls.
Declare the template method final when the skeleton must not change. C++11's final prevents accidental override of the invariant skeleton:
class Base {
public:
void execute() final { step_a(); step_b(); step_c(); }
private:
virtual void step_a() = 0;
virtual void step_b() {}
virtual void step_c() {}
};Always mark overrides with override. C++11's override catches misspelled hook names and signature mismatches at compile time. Never omit it in derived classes.
Provide a virtual destructor. Any polymorphic base class whose objects may be deleted through a base pointer requires a virtual destructor. Omitting it is undefined behavior. A protected non-virtual destructor is an alternative when deletion through the base is intentionally prohibited.
Return values through hooks, not side effects. Hooks that communicate results by mutating shared state create hidden coupling between steps. Pass data explicitly via parameters and return values — the clean → validate → transform chain above demonstrates this.
Common Pitfalls
Calling virtual hooks from constructors or destructors. During base-class construction, dynamic dispatch resolves to the base implementation, never the derived override. Code expecting on_init() to call the derived on_init() from a constructor will silently call the base no-op instead.
class Broken {
public:
Broken() { on_init(); } // always calls Broken::on_init(), not derived
virtual void on_init() {}
};Use two-phase initialization: construct the object, then call the template method as a separate step.
Making the template method itself virtual. If run() or generate() is virtual, a derived class can override the entire skeleton, discarding every invariant the base enforces. The skeleton must be non-virtual for NVI to have any meaning.
Excessive hooks create fragile base class syndrome. Each hook is a coupling point. A base class with a dozen hooks effectively becomes an abstract interface with hidden ordering constraints. If most hooks must be overridden for a subclass to be correct, the abstraction is wrong — consider Strategy or policy-based design instead.
CRTP does not support runtime polymorphism. Encoder<Base64Encoder> and Encoder<GzipEncoder> are unrelated types. You cannot store them in a std::vector<Encoder*> or pass them through a common interface without adding a separate virtual base. For heterogeneous collections, use virtual dispatch.
Missing friend declarations in CRTP hooks. If do_encode is private in the derived class, self().do_encode(...) in the base fails to compile. Either make CRTP hooks protected, or add friend class Encoder<Derived> in the derived class.
See Also
- CRTP — the static polymorphism mechanism underpinning the zero-overhead variant
- Policy-Based Design — compile-time step customization via template parameters rather than inheritance
- Strategy Pattern — swaps the entire algorithm rather than individual steps; prefer when the skeleton itself varies