Advanced C++ Features: Polymorphism, Overloading, and Generics
Runtime Polymorphism with Abstract Base Classes
Defining an abstract base class allows for a unified interface across diverse derived types. The following hierarchy demonstrates a content management system where different media types share common behaviors but implement them uniquely.
Media Hierarchy Implementation
The header file defines the interface. Note the use of pure virtual functions to enforce implementation in derived classes.
#pragma once
#include <string>
class ContentNode {
public:
explicit ContentNode(const std::string &title_);
virtual ~ContentNode() = default;
virtual void display_info() const = 0;
virtual void consume() const = 0;
protected:
std::string title;
};
class Novel : public ContentNode {
public:
Novel(const std::string &title_, const std::string &writer_);
void display_info() const override;
void consume() const override;
private:
std::string writer;
};
class Movie : public ContentNode {
public:
Movie(const std::string &title_, const std::string &director_);
void display_info() const override;
void consume() const override;
private:
std::string director;
};
class Song : public ContentNode {
public:
Song(const std::string &title_, const std::string &performer_);
void display_info() const override;
void consume() const override;
private:
std::string performer;
};
The implementation file provides the logic for each media type. Each class initializes its base class and specific members via the constructor initialization list.
#include <iostream>
#include "content_node.hpp"
ContentNode::ContentNode(const std::string &title_) : title{title_} {}
Novel::Novel(const std::string &title_, const std::string &writer_)
: ContentNode{title_}, writer{writer_} {}
void Novel::display_info() const {
std::cout << "Release: Novel [" << title << "] by " << writer << '\n';
}
void Novel::consume() const {
std::cout << "Action: Reading [" << title << "]\n";
}
Movie::Movie(const std::string &title_, const std::string &director_)
: ContentNode{title_}, director{director_} {}
void Movie::display_info() const {
std::cout << "Release: Movie <" << title << "> directed by " << director << '\n';
}
void Movie::consume() const {
std::cout << "Action: Watching <" << title << ">\n";
}
Song::Song(const std::string &title_, const std::string &performer_)
: ContentNode{title_}, performer{performer_} {}
void Song::display_info() const {
std::cout << "Release: Track " << title << " by " << performer << '\n';
}
void Song::consume() const {
std::cout << "Action: Listening to " << title << '\n';
}
Memory Management Strategies
When storing polymorphic objects, raw pointers require manual deletion, whereas smart pointers automate resource management. The virtual destructor in the base class ensures derived destructors are called correctly.
#include <memory>
#include <vector>
#include <iostream>
#include "content_node.hpp"
void demonstrate_raw_pointers() {
std::vector<ContentNode*> collection;
collection.push_back(new Novel("Dune", "Frank Herbert"));
collection.push_back(new Movie("Inception", "Christopher Nolan"));
for(auto* item : collection) {
item->display_info();
item->consume();
delete item;
}
}
void demonstrate_smart_pointers() {
std::vector<std::unique_ptr<ContentNode>> collection;
collection.push_back(std::make_unique<Novel("1984", "George Orwell"));
collection.push_back(std::make_unique<Song("Bohemian Rhapsody", "Queen"));
for(const auto& item : collection) {
item->display_info();
item->consume();
}
}
int main() {
demonstrate_smart_pointers();
return 0;
}
Operator Overloading and Object Composition
Complex data structures often require custom output formatting and composition relationships. This section illustrates a sales tracking system where a transaction record contains a product description.
Product and Sales Record Classes
The Product class holds static information, while SalesRecord combines product data with dynamic sales metrics. The insertion operator is overloaded to facilitate clean printing.
#pragma once
#include <string>
#include <iostream>
class Product {
public:
Product(const std::string &n, const std::string &a,
const std::string &i, double p);
friend std::ostream& operator<<(std::ostream &out, const Product &p);
private:
std::string name;
std::string author;
std::string isbn;
double price;
};
class SalesRecord {
public:
SalesRecord(const Product &p, double sell_price, int qty);
int get_quantity() const;
double calculate_revenue() const;
friend std::ostream& operator<<(std::ostream &out, const SalesRecord &rec);
private:
Product item;
double sell_price;
int quantity;
};
#include <iomanip>
#include "sales.hpp"
Product::Product(const std::string &n, const std::string &a,
const std::string &i, double p)
: name{n}, author{a}, isbn{i}, price{p} {}
std::ostream& operator<<(std::ostream &out, const Product &p) {
out << std::left << std::setw(10) << "Title:" << p.name << '\n'
<< std::setw(10) << "Author:" << p.author << '\n'
<< std::setw(10) << "ISBN:" << p.isbn << '\n'
<< std::setw(10) << "List Price:" << p.price;
return out;
}
SalesRecord::SalesRecord(const Product &p, double sell_price, int qty)
: item{p}, sell_price{sell_price}, quantity{qty} {}
double SalesRecord::calculate_revenue() const {
return quantity * sell_price;
}
std::ostream& operator<<(std::ostream &out, const SalesRecord &rec) {
out << rec.item << '\n'
<< std::setw(10) << "Sold For:" << rec.sell_price << '\n'
<< std::setw(10) << "Volume:" << rec.quantity << '\n'
<< std::setw(10) << "Total:" << rec.calculate_revenue();
return out;
}
Sorting Sales Data
Records can be organized using standard algorithms. A custom comparator allows sorting based on specific metrics like sales volume.
#include <vector>
#include <algorithm>
#include "sales.hpp"
bool compare_by_volume(const SalesRecord &a, const SalesRecord &b) {
return a.get_quantity() > b.get_quantity();
}
void process_sales_data() {
std::vector<SalesRecord> ledger;
// Assume data population here
// ledger.push_back(...);
std::sort(ledger.begin(), ledger.end(), compare_by_volume);
for(const auto &entry : ledger) {
std::cout << entry << "\n----------------\n";
}
}
Generic Programming with Class Templates
Templates eliminate code duplication when logic is identical across different data types. Comparing specific classes against a generic template highlights the reduction in boilerplate code.
Specific vs. Generic Coordinates
Without templates, separate classes are needed for integers and doubles. A template class generalizes this behavior.
#include <iostream>
#include <string>
// Specific implementation for integers
class CoordInt {
public:
CoordInt(int x, int y) : x_val{x}, y_val{y} {}
void show() const { std::cout << x_val << ", " << y_val << '\n'; }
private:
int x_val, y_val;
};
// Specific implementation for doubles
class CoordDouble {
public:
CoordDouble(double x, double y) : x_val{x}, y_val{y} {}
void show() const { std::cout << x_val << ", " << y_val << '\n'; }
private:
double x_val, y_val;
};
// Generic template implementation
template<typename T>
class CoordGeneric {
public:
CoordGeneric(T x, T y) : x_val{x}, y_val{y} {}
void show() const { std::cout << x_val << ", " << y_val << '\n'; }
private:
T x_val, y_val;
};
int main() {
CoordInt ci(10, 20);
ci.show();
CoordGeneric<double> cg(10.5, 20.5);
cg.show();
CoordGeneric<std::string> cs("A", "B");
cs.show();
return 0;
}
Arithmetic Templates: Complex Number Example
Templates are particularly useful for mathematical structures where operations remain consistent regardless of the underlying scalar type. The following class supports arithmetic operations and stream I/O.
Template Class Definition
This implementation handles real and imaginary parts generically. Friend functions are used to allow access to private members during operator overloading.
#ifndef NUMERIC_COMPLEX_HPP
#define NUMERIC_COMPLEX_HPP
#include <iostream>
template<typename T>
class NumericComplex {
private:
T re;
T im;
public:
NumericComplex() : re(0), im(0) {}
NumericComplex(T r, T i = 0) : re(r), im(i) {}
NumericComplex(const NumericComplex& other) : re(other.re), im(other.im) {}
T real_part() const { return re; }
T imag_part() const { return im; }
NumericComplex& operator+=(const NumericComplex& other) {
re += other.re;
im += other.im;
return *this;
}
friend NumericComplex operator+(const NumericComplex& l, const NumericComplex& r) {
return NumericComplex(l.re + r.re, l.im + r.im);
}
bool operator==(const NumericComplex& other) const {
return (re == other.re) && (im == other.im);
}
friend std::ostream& operator<<(std::ostream& os, const NumericComplex<T>& val) {
os << val.re;
if (val.im >= T(0)) {
os << " + " << val.im << "i";
} else {
os << " - " << (-val.im) << "i";
}
return os;
}
friend std::istream& operator>>(std::istream& is, NumericComplex<T>& val) {
T r, i;
if (is >> r >> i) {
val.re = r;
val.im = i;
}
return is;
}
};
#endif
Usage Demonstration
The template works seamlessly with both integer and floating-point types, demonstrating type safety and code reuse.
#include "NumericComplex.hpp"
void run_tests() {
NumericComplex<int> c1(2, -5);
NumericComplex<int> c2 = c1;
std::cout << "Value 1: " << c1 << '\n';
std::cout << "Value 2: " << c2 << '\n';
std::cout << "Sum: " << (c1 + c2) << '\n';
c1 += c2;
std::cout << "Updated Value 1: " << c1 << '\n';
std::cout << "Equality Check: " << (c1 == c2) << '\n';
}
int main() {
run_tests();
return 0;
}