Overview
This implementation shows the Template Method pattern with virtual methods in an inheritance hierarchy. The classic approach defines an abstract base class with a template method that orchestrates the algorithm, and concrete subclasses override the customizable steps.
Benefits of this approach
- Explicit algorithm structure. The template method shows the algorithm's flow directly.
- Compile-time type safety. The compiler checks every custom step.
- Direct virtual calls. No runtime function wrapping or extra overhead.
- Clear contracts. The abstract methods state exactly what subclasses must implement.
Traditional approach: virtual methods
The classic Template Method pattern uses inheritance with virtual functions.
Base class with virtual methods
#include <iostream>
#include <string>
class DataProcessorBase {
public:
virtual ~DataProcessorBase() = default;
// Template method: defines algorithm skeleton
void execute(const std::string& input) {
std::cout << "Starting data processing pipeline...\n";
// Step 1: Validate
std::string validated = validate(input);
if (validated.empty()) {
std::cout << "Validation failed. Aborting.\n";
return;
}
// Step 2: Process
std::string processed = process(validated);
std::cout << "Processing complete.\n";
// Step 3: Save
save(processed);
std::cout << "Pipeline finished successfully.\n";
}
protected:
// Virtual methods: subclasses override these
virtual std::string validate(const std::string& data) = 0;
virtual std::string process(const std::string& data) = 0;
virtual void save(const std::string& data) = 0;
};
Concrete implementations
class CSVProcessor : public DataProcessorBase {
protected:
std::string validate(const std::string& data) override {
std::cout << " [CSV Validate] Checking CSV format...\n";
return data.empty() ? "" : data;
}
std::string process(const std::string& data) override {
std::cout << " [CSV Process] Parsing and cleaning CSV...\n";
return data + " (cleaned)";
}
void save(const std::string& data) override {
std::cout << " [CSV Save] Writing to CSV file: " << data << "\n";
}
};
class JSONProcessor : public DataProcessorBase {
protected:
std::string validate(const std::string& data) override {
std::cout << " [JSON Validate] Validating JSON syntax...\n";
if (data.empty() || data[0] != '{') return "";
return data;
}
std::string process(const std::string& data) override {
std::cout << " [JSON Process] Parsing and formatting JSON...\n";
return data + " (formatted)";
}
void save(const std::string& data) override {
std::cout << " [JSON Save] Writing to JSON file: " << data << "\n";
}
};
Traditional usage example
int main() {
std::cout << "=== Template Method Pattern (Virtual Methods) ===\n\n";
// CSV Pipeline
std::cout << "1. CSV Pipeline:\n";
CSVProcessor csvProc;
csvProc.execute("name,age,email");
std::cout << "\n2. JSON Pipeline:\n";
JSONProcessor jsonProc;
jsonProc.execute("{\"key\": \"value\"}");
return 0;
}
Output
=== Template Method Pattern (Virtual Methods) ===
1. CSV Pipeline:
Starting data processing pipeline...
[CSV Validate] Checking CSV format...
[CSV Process] Parsing and cleaning CSV...
Processing complete.
[CSV Save] Writing to CSV file: name,age,email (cleaned)
Pipeline finished successfully.
2. JSON Pipeline:
Starting data processing pipeline...
[JSON Validate] Validating JSON syntax...
[JSON Process] Parsing and formatting JSON...
Processing complete.
[JSON Save] Writing to JSON file: {"key": "value"} (formatted)
Pipeline finished successfully.
Key points: virtual methods approach
Template Method pattern
The execute() method is the template method that defines the algorithm skeleton. It calls virtual methods at specific points where subclasses can inject custom behavior.
Abstract methods
Protected pure virtual methods (validate(), process(), save()) force subclasses to implement every customizable step.
Inheritance hierarchy
Each concrete processor inherits from DataProcessorBase and implements the virtual methods. The algorithm structure is the same in every implementation.
Type safety
The compiler checks all customizations and verifies that concrete classes implement every abstract method.
Modern approach: std::function (composition)
The traditional inheritance approach suits tightly coupled hierarchies. Modern C++ has an alternative in std::function and composition, which adds flexibility without inheritance.
The algorithm class with std::function
#include <iostream>
#include <functional>
#include <string>
class DataProcessor {
private:
// Template method steps as std::function members
std::function<std::string(const std::string&)> validate;
std::function<std::string(const std::string&)> process;
std::function<void(const std::string&)> save;
public:
// Constructor accepts all customizable steps
DataProcessor(
std::function<std::string(const std::string&)> validateFn,
std::function<std::string(const std::string&)> processFn,
std::function<void(const std::string&)> saveFn
)
: validate(validateFn), process(processFn), save(saveFn) {}
// Template method: defines algorithm skeleton
void execute(const std::string& input) {
std::cout << "Starting data processing pipeline...\n";
// Step 1: Validate
std::string validated = validate(input);
if (validated.empty()) {
std::cout << "Validation failed. Aborting.\n";
return;
}
// Step 2: Process
std::string processed = process(validated);
std::cout << "Processing complete.\n";
// Step 3: Save
save(processed);
std::cout << "Pipeline finished successfully.\n";
}
};
Function-based implementations
class CSVProcessorFn {
public:
static std::string validateCSV(const std::string& data) {
std::cout << " [CSV Validate] Checking CSV format...\n";
if (data.empty()) return "";
return data;
}
static std::string processCSV(const std::string& data) {
std::cout << " [CSV Process] Parsing and cleaning CSV...\n";
return data + " (cleaned)";
}
static void saveCSV(const std::string& data) {
std::cout << " [CSV Save] Writing to CSV file: " << data << "\n";
}
};
class JSONProcessorFn {
public:
static std::string validateJSON(const std::string& data) {
std::cout << " [JSON Validate] Validating JSON syntax...\n";
if (data.empty() || data[0] != '{') return "";
return data;
}
static std::string processJSON(const std::string& data) {
std::cout << " [JSON Process] Parsing and formatting JSON...\n";
return data + " (formatted)";
}
static void saveJSON(const std::string& data) {
std::cout << " [JSON Save] Writing to JSON file: " << data << "\n";
}
};
Using lambda functions
class CustomProcessor {
public:
static DataProcessor createWithLambdas() {
return DataProcessor(
// Validate: reject if contains special chars
[](const std::string& data) -> std::string {
std::cout << " [Lambda Validate] Checking for special characters...\n";
if (data.find('!') != std::string::npos) return "";
return data;
},
// Process: convert to uppercase
[](const std::string& data) -> std::string {
std::cout << " [Lambda Process] Converting to uppercase...\n";
std::string result = data;
for (char& c : result) c = std::toupper(c);
return result;
},
// Save: print to console
[](const std::string& data) {
std::cout << " [Lambda Save] Output: " << data << "\n";
}
);
}
};
Composition usage example
int main() {
std::cout << "=== Template Method Pattern (std::function) ===\n\n";
// CSV Pipeline
std::cout << "1. CSV Pipeline:\n";
DataProcessor csvPipeline(
CSVProcessorFn::validateCSV,
CSVProcessorFn::processCSV,
CSVProcessorFn::saveCSV
);
csvPipeline.execute("name,age,email");
std::cout << "\n2. JSON Pipeline:\n";
DataProcessor jsonPipeline(
JSONProcessorFn::validateJSON,
JSONProcessorFn::processJSON,
JSONProcessorFn::saveJSON
);
jsonPipeline.execute("{\"key\": \"value\"}");
std::cout << "\n3. Lambda-based Pipeline:\n";
DataProcessor customPipeline = CustomProcessor::createWithLambdas();
customPipeline.execute("hello");
std::cout << "\n4. Failed Validation Example:\n";
customPipeline.execute("invalid!");
return 0;
}
Output (std::function)
=== Template Method Pattern (std::function) ===
1. CSV Pipeline:
Starting data processing pipeline...
[CSV Validate] Checking CSV format...
[CSV Process] Parsing and cleaning CSV...
Processing complete.
[CSV Save] Writing to CSV file: name,age,email (cleaned)
Pipeline finished successfully.
2. JSON Pipeline:
Starting data processing pipeline...
[JSON Validate] Validating JSON syntax...
[JSON Process] Parsing and formatting JSON...
Processing complete.
[JSON Save] Writing to JSON file: {"key": "value"} (formatted)
Pipeline finished successfully.
3. Lambda-based Pipeline:
Starting data processing pipeline...
[Lambda Validate] Checking for special characters...
[Lambda Process] Converting to uppercase...
Processing complete.
[Lambda Save] Output: HELLO
Pipeline finished successfully.
4. Failed Validation Example:
Starting data processing pipeline...
[Lambda Validate] Checking for special characters...
Validation failed. Aborting.
Key points: std::function approach
Constructor dependency injection
The DataProcessor constructor accepts all customizable steps up front:
DataProcessor(validateFn, processFn, saveFn);
This makes dependencies explicit and lets you pass mock functions in tests.
Flexibility with std::function
The std::function type can hold:
- Function pointers
- Lambda expressions
- Member function pointers (with binding)
- Function objects (functors)
Type safety
Each step has a clear signature:
std::function<std::string(const std::string&)>for validation and processingstd::function<void(const std::string&)>for save operations
Separation of concerns
- The template method defines the algorithm flow.
- Each processor implementation focuses on its own domain logic.
- The main code only wires the steps together.
Comparison: virtual methods vs std::function
| Aspect | Virtual Methods | std::function (Composition) |
|---|---|---|
| Coupling | Tight, hierarchical | Loose, flexible |
| Inheritance | Required | Not required |
| Testing | Requires mock subclasses | Easy with lambdas/mocks |
| Runtime changes | No | Yes, trivial |
| Memory overhead | Virtual table per class | std::function wrapper |
| Complexity | Class hierarchies | Simple function composition |
| Reusability | Through subclassing | Through function composition |
| Modern C++ style | Traditional OOP | Functional programming |
| Type safety | Compile-time checked | Runtime type erasure |
| Performance | Direct virtual calls | Small function call overhead |
When to use
Virtual methods approach:
- Tightly integrated class hierarchies
- You need compile-time type safety
- Performance is critical and function wrapping overhead matters
- Traditional OOP designs
std::function approach:
- Flexible plugin architectures
- You need runtime behavior changes
- Testing with mock functions
- You prefer functional programming style
Summary
Use virtual methods when the steps belong to a type and the hierarchy is stable. Use std::function when callers need to replace a step at runtime or when a subclass would exist only to supply three functions. The algorithm skeleton stays the same in both versions, which is the point of Template Method.