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 processing
  • std::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.