C++ Design Patterns: Inheritance vs Composition, Templates, and Standard Library Integration

Inheritance vs Composition for Data Management

Consider a scenario where a class is designed to manage student grades. A common design mistake is inheriting from a standard container. Below is an implementation of VectorGradeAdapter, which inherits publicly from std::vector<int>.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <iomanip>

class VectorGradeAdapter : public std::vector<int> {
public:
    VectorGradeAdapter(const std::string &title, int count)
        : course_title(title), student_count(count) {}

    void load_scores() {
        int score;
        for (int i = 0; i < student_count; ++i) {
            std::cin >> score;
            this->push_back(score);
        }
    }

    void display_scores() const {
        for (auto it = this->begin(); it != this->end(); ++it) {
            std::cout << *it << " ";
        }
        std::cout << std::endl;
    }

    void sort_scores(bool ascending = false) {
        if (ascending) {
            std::sort(this->begin(), this->end());
        } else {
            std::sort(this->begin(), this->end(), std::greater<int>());
        }
    }

    int find_min() const {
        return *std::min_element(this->begin(), this->end());
    }

    int find_max() const {
        return *std::max_element(this->begin(), this->end());
    }

    double calculate_average() const {
        return std::accumulate(this->begin(), this->end(), 0) * 1.0 / student_count;
    }

    void generate_report() {
        std::cout << "Course: " << course_title << std::endl;
        sort_scores();
        display_scores();
        std::cout << "Max: " << find_max() << std::endl;
        std::cout << "Min: " << find_min() << std::endl;
        std::cout << "Avg: " << std::fixed << std::setprecision(2) << calculate_average() << std::endl;
        analyze_distribution();
    }

private:
    void analyze_distribution() {
        std::vector<int> distribution(5, 0);
        for (int score : *this) {
            if (score < 60) distribution[0]++;
            else if (score < 70) distribution[1]++;
            else if (score < 80) distribution[2]++;
            else if (score < 90) distribution[3]++;
            else distribution[4]++;
        }
        // Output logic omitted for brevity
    }

    std::string course_title;
    int student_count;
};
In this design, VectorGradeAdapter exposes the full interface of std::vector. Data is stored in the base class part of the object. Methods like load_scores access the underlying storage via the inherited push_back method, while algorithms use iterators provided by the base class. However, this design is often discouraged because standard containers do not have virtual destructors, leading to potential undefined behavior if deletion occurs through a base pointer. Furthermore, it violates the "is-a" principle; a grade manager is not conceptually a vector. A more robust approach uses composition. The container becomes a member variable, hiding implementation details and enforcing a stricter interface.
class CompositeGradeBook {
public:
    CompositeGradeBook(const std::string &title, int count)
        : course_title(title), student_count(count) {}

    void load_scores() {
        int score;
        for (int i = 0; i < student_count; ++i) {
            std::cin >> score;
            grades.push_back(score);
        }
    }

    // ... other methods operating on 'grades' member ...

private:
    std::string course_title;
    int student_count;
    std::vector<int> grades; // Composition
    std::vector<int> distribution_counts;
    std::vector<double> distribution_rates;
};
With composition, the internal representation (e.g., changing from vector to deque) does not affect the class user. Access to data is strictly controlled through member functions, enhancing encapsulation.

Standard Library: Input Stream Handling

Handling user input often requires distinguishing between formatted extraction (cin >>) and line-based reading (getline). The standard extraction operator stops at whitespace, while getline reads until a newline character.
#include <iostream>
#include <string>
#include <limits>

void input_demo() {
    std::string first, second;
    
    // Extraction stops at space
    std::cin >> first >> second; 
    
    // Clear buffer residue
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    
    // Read full line
    std::getline(std::cin, first);
}
The ignore function is critical when switching between formatted input and getline. Without it, leftover newline characters in the input buffer can cause getline to return an empty string immediately.

Generic Programming with Class Templates

Templates allow creating classes that work with arbitrary data types. A resource manager template can handle different numeric types for game stats.
#pragma once

template <typename T>
class StatManager {
public:
    StatManager(T initial) : current_value(initial) {}

    T get_value() const { return current_value; }

    void modify(T delta) {
        current_value += delta;
        if (current_value < 0) {
            current_value = 0;
        }
    }

private:
    T current_value;
};
This template can be instantiated with float for health points or int for gold, centralizing the logic for value clamping and updates.

Object-Oriented Banking System

A more complex system, such as a banking application, demonstrates deep inheritance hierarchies and object composition. The system relies on a foundation date class.
// date_utils.h
#include <iostream>

class CalendarDate {
private:
    int year, month, day;
    int total_days;

public:
    CalendarDate(int y, int m, int d);
    int get_year() const { return year; }
    int get_month() const { return month; }
    int get_day() const { return day; }
    
    bool is_leap() const {
        return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    }
    
    int distance(const CalendarDate &other) const {
        return total_days - other.total_days;
    }
    
    void print() const;
};
An accumulator utility class assists in calculating interest over time.
// accumulator.h
class ValueAccumulator {
private:
    CalendarDate last_update;
    double value;
    double sum;

public:
    ValueAccumulator(const CalendarDate &date, double val)
        : last_update(date), value(val), sum(0) {}

    double get_sum(const CalendarDate &date) const {
        return sum + value * date.distance(last_update);
    }

    void change(const CalendarDate &date, double val) {
        sum = get_sum(date);
        last_update = date;
        value = val;
    }
};
The account hierarchy starts with an abstract base managing common details like ID and balance.
// account_base.h
class BankAccount {
protected:
    std::string id;
    double balance;
    static double total_assets;

    void record_transaction(const CalendarDate &date, double amount, const std::string &desc);
    void report_error(const std::string &msg) const;

public:
    BankAccount(const CalendarDate &date, const std::string &id);
    const std::string &get_id() const { return id; }
    double get_balance() const { return balance; }
    static double get_total_assets() { return total_assets; }
    virtual void display() const;
};
Derived classes implement specific behaviors. A SavingsAccount accrues interest annually.
class SavingsAccount : public BankAccount {
private:
    ValueAccumulator accumulator;
    double interest_rate;

public:
    SavingsAccount(const CalendarDate &date, const std::string &id, double rate);
    void deposit(const CalendarDate &date, double amount, const std::string &desc);
    void withdraw(const CalendarDate &date, double amount, const std::string &desc);
    void settle_interest(const CalendarDate &date);
};
A CreditAccount manages debt, credit limits, and fees.
class CreditAccount : public BankAccount {
private:
    ValueAccumulator accumulator;
    double credit_limit;
    double daily_rate;
    double annual_fee;

    double get_debt() const {
        return balance < 0 ? balance : 0;
    }

public:
    CreditAccount(const CalendarDate &date, const std::string &id, double limit, double rate, double fee);
    void deposit(const CalendarDate &date, double amount, const std::string &desc);
    void withdraw(const CalendarDate &date, double amount, const std::string &desc);
    void settle_interest(const CalendarDate &date);
    void display() const override;
};
This structure allows for distinct logic: SavingsAccount restricts withdrawals to the available balance, while CreditAccount allows overdrafts up to the credit_limit. The settle_interest methods differ significantly; savings calculate interest on positive balances, while credit accounts calculate interest on debt. The ValueAccumulator helper class is composed into both derived classes to handle the day-by-day accumulation logic efficiently without code duplication.

Tags: C++ Object-Oriented Programming Inheritance composition Templates

Posted on Sun, 27 Sep 2026 16:39:09 +0000 by mackevin