Advanced OOP in C++: Operator Overloading, Virtual Inheritance, Abstract Classes & Design Patterns
⚡ Quick Answer: What are advanced OOP concepts in C++?
- Operator overloading — redefine
+,==,<<for your classes - Virtual inheritance — solve the diamond problem in multiple inheritance
- Abstract classes & pure virtual functions — enforce interfaces via
= 0 - Friend functions & classes — grant selective private-member access
- RTTI & dynamic_cast — safely determine object types at runtime
- Design patterns — Singleton, Factory, Strategy for scalable DSA code
Mastering advanced OOP in C++ is the jump from writing code that works to writing code that scales. These features are the backbone of production software, competitive programming libraries, and every major DSA implementation. This guide covers all six advanced concepts with complete working examples, DSA applications, common mistakes, and interview-ready explanations.
Operator Overloading
Redefine +, ==, << for classes
Virtual Inheritance
Diamond problem solution
Abstract Classes
Pure virtual functions
Friend Functions
Private member access
RTTI & dynamic_cast
Runtime type checking
Design Patterns
Singleton, Factory, Strategy
Operator Overloading in C++
Operator overloading lets you redefine what built-in operators (+,
==, <<, [], etc.) do when applied to objects of
your class. Without overloading, Complex c3 = c1 + c2 is a compile error; with it,
it reads as naturally as adding two integers.
Syntax: member function vs free function
Use a member function when the left operand is always your class. Use a
free function (or friend) when the left operand may not be your class —
the most common case is the stream operator <<, where the left operand is
std::ostream, not your class.
#include <iostream>
using namespace std;
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
Complex operator+(const Complex& rhs) const {
return Complex(real + rhs.real, imag + rhs.imag);
}
bool operator==(const Complex& rhs) const {
return real == rhs.real && imag == rhs.imag;
}
friend ostream& operator<<(ostream& os, const Complex& c) {
return os << c.real << " + " << c.imag << "i";
}
};
int main() {
Complex c1(3.0, 4.0), c2(1.0, 2.0);
Complex c3 = c1 + c2;
cout << "Sum: " << c3 << endl;
cout << "Equal: " << (c1 == c2 ? "yes" : "no") << endl;
return 0;
}
Sum: 4 + 6i
Equal: no// Use a free function when the left operand is NOT your class
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
};
Complex operator+(double lhs, const Complex& rhs) {
return Complex(lhs + rhs.real, rhs.imag);
}
int main() {
Complex c(3.0, 4.0);
Complex result = 2.0 + c;
return 0;
}
Operator overloading for DSA and STL compatibility
#include <iostream>
#include <set>
#include <queue>
using namespace std;
struct Node {
int id, cost;
bool operator<(const Node& o) const { return cost < o.cost; }
bool operator==(const Node& o) const { return id == o.id && cost == o.cost; }
friend ostream& operator<<(ostream& os, const Node& n) {
return os << "Node(" << n.id << ", cost=" << n.cost << ")";
}
};
int main() {
set<Node> nodes = {{1,10},{2,5},{3,15}};
for (const auto& n : nodes) cout << n << " ";
cout << endl;
priority_queue<Node, vector<Node>, greater<Node>> pq;
pq.push({1,10}); pq.push({2,3}); pq.push({3,7});
while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); }
cout << endl;
return 0;
}
Node(2, cost=5) Node(1, cost=10) Node(3, cost=15)
Node(2, cost=3) Node(3, cost=7) Node(1, cost=10)Quick Reference: Which operators can be overloaded?
| Operator(s) | Category | Typical return type | Can overload? |
|---|---|---|---|
| + - * / % | Arithmetic | MyClass (new object) | ✓ Yes |
| == != < > <= >= | Comparison | bool | ✓ Yes |
| = += -= *= /= | Assignment | MyClass& (*this) | ✓ Yes |
| ++ -- (pre & post) | Increment | MyClass& (pre) / MyClass (post) | ✓ Yes |
| << >> | Stream I/O | ostream& / istream& | ✓ Yes (friend) |
| [] | Subscript | T& | ✓ Yes |
| () | Function call | Any (functor) | ✓ Yes |
| -> * | Pointer ops | T* / T& | ✓ Yes |
| :: .* . ?: | Control | — | ✗ No |
| sizeof alignof typeid | Type | — | ✗ No |
Virtual Inheritance & the Diamond Problem
What is the diamond problem?
The diamond problem occurs when class D inherits from B and C, and both inherit from A. Without virtual inheritance, D gets two copies of A's data, causing ambiguity and wasted memory.
: public A
: public A
: public B, public C
virtual public A: D has ONE shared copy of A ✓#include <iostream>
using namespace std;
class Graph {
public:
int numVertices;
Graph(int v) : numVertices(v) {}
virtual void display() { cout << "Graph with " << numVertices << " vertices\n"; }
};
class DirectedGraph : virtual public Graph {
public: DirectedGraph(int v) : Graph(v) {}
};
class WeightedGraph : virtual public Graph {
public: WeightedGraph(int v) : Graph(v) {}
};
class DirectedWeightedGraph : public DirectedGraph, public WeightedGraph {
public:
DirectedWeightedGraph(int v) : Graph(v), DirectedGraph(v), WeightedGraph(v) {}
};
int main() {
DirectedWeightedGraph g(10);
g.display();
cout << "Vertices: " << g.numVertices << endl;
return 0;
}
Graph with 10 vertices
Vertices: 10DirectedWeightedGraph) is responsible for constructing the virtual base class (Graph) directly. Always call the virtual base constructor explicitly in the most-derived class.
Abstract Classes & Pure Virtual Functions
An abstract class has at least one pure virtual function (declared with = 0). It cannot be instantiated directly — it defines an interface that all derived classes must implement.
| Feature | virtual function | pure virtual (= 0) |
|---|---|---|
| Has implementation | Yes, in base class | No |
| Must override in derived | No (optional) | Yes (or derived is also abstract) |
| Base class instantiable | Yes | No (abstract) |
| Enables runtime polymorphism | Yes | Yes |
#include <iostream>
#include <vector>
using namespace std;
class GraphTraversal {
public:
virtual void traverse(int start) = 0;
virtual string name() const = 0;
virtual ~GraphTraversal() {}
};
class BFS : public GraphTraversal {
public:
void traverse(int start) override { cout << "BFS from vertex " << start << endl; }
string name() const override { return "Breadth-First Search"; }
};
class DFS : public GraphTraversal {
public:
void traverse(int start) override { cout << "DFS from vertex " << start << endl; }
string name() const override { return "Depth-First Search"; }
};
int main() {
vector<GraphTraversal*> algos = {new BFS(), new DFS()};
for (auto* algo : algos) {
cout << algo->name() << ": ";
algo->traverse(0);
delete algo;
}
return 0;
}
Breadth-First Search: BFS from vertex 0
Depth-First Search: DFS from vertex 0Friend Functions & Classes
A friend function is a non-member function that has access to a class's private and protected members. Declare it inside the class with the friend keyword.
#include <iostream>
#include <vector>
using namespace std;
class AdjacencyList {
private:
int vertices;
vector<vector<int>> adj;
public:
AdjacencyList(int v) : vertices(v), adj(v) {}
void addEdge(int u, int v) { adj[u].push_back(v); adj[v].push_back(u); }
friend ostream& operator<<(ostream& os, const AdjacencyList& g) {
for (int i = 0; i < g.vertices; i++) {
os << i << ": ";
for (int v : g.adj[i]) os << v << " ";
os << "\n";
}
return os;
}
};
int main() {
AdjacencyList g(4);
g.addEdge(0,1); g.addEdge(0,2); g.addEdge(1,3);
cout << g;
return 0;
}
0: 1 2
1: 0 3
2: 0
3: 1 RTTI & dynamic_cast
Runtime Type Identification (RTTI) allows determining an object's actual (dynamic) type at runtime. C++ provides two tools: typeid(obj) and dynamic_cast.
#include <iostream>
#include <vector>
#include <typeinfo>
using namespace std;
class ASTNode {
public:
virtual ~ASTNode() {}
virtual string type() const = 0;
};
class NumberNode : public ASTNode {
public:
int value;
NumberNode(int v) : value(v) {}
string type() const override { return "Number"; }
};
class OperatorNode : public ASTNode {
public:
char op;
OperatorNode(char o) : op(o) {}
string type() const override { return "Operator"; }
};
int main() {
vector<ASTNode*> nodes = {
new NumberNode(42), new OperatorNode('+'), new NumberNode(7)
};
for (auto* node : nodes) {
cout << "Type: " << node->type() << " | ";
if (auto* num = dynamic_cast<NumberNode*>(node))
cout << "Value = " << num->value;
else if (auto* op = dynamic_cast<OperatorNode*>(node))
cout << "Op = " << op->op;
cout << endl;
delete node;
}
return 0;
}
Type: Number | Value = 42
Type: Operator | Op = +
Type: Number | Value = 7dynamic_cast chains, the correct fix is usually to add a virtual method to the base class.
Design Patterns in C++ OOP
Design patterns are proven, reusable solutions to common software design problems. The three most important for DSA are Singleton, Factory, and Strategy.
Singleton Pattern — thread-safe (C++11)
#include <iostream>
using namespace std;
class GraphConfig {
private:
int maxVertices;
GraphConfig() : maxVertices(1000) {}
public:
static GraphConfig& getInstance() {
static GraphConfig instance;
return instance;
}
GraphConfig(const GraphConfig&) = delete;
GraphConfig& operator=(const GraphConfig&) = delete;
int getMax() const { return maxVertices; }
void setMax(int m) { maxVertices = m; }
};
int main() {
GraphConfig& cfg1 = GraphConfig::getInstance();
cfg1.setMax(500);
GraphConfig& cfg2 = GraphConfig::getInstance();
cout << "Max: " << cfg2.getMax() << endl;
cout << "Same? " << (&cfg1 == &cfg2 ? "yes" : "no") << endl;
return 0;
}
Max: 500
Same? yesFactory Pattern
#include <iostream>
#include <memory>
using namespace std;
class IGraph {
public:
virtual void addEdge(int u, int v) = 0;
virtual string typeName() const = 0;
virtual ~IGraph() = default;
};
class DirectedGraph : public IGraph {
public:
void addEdge(int u, int v) override { cout << "Directed: " << u << " → " << v << endl; }
string typeName() const override { return "Directed"; }
};
class UndirectedGraph : public IGraph {
public:
void addEdge(int u, int v) override { cout << "Undirected: " << u << " — " << v << endl; }
string typeName() const override { return "Undirected"; }
};
unique_ptr<IGraph> createGraph(const string& type) {
if (type == "directed") return make_unique<DirectedGraph>();
if (type == "undirected") return make_unique<UndirectedGraph>();
return nullptr;
}
int main() {
auto g1 = createGraph("directed");
auto g2 = createGraph("undirected");
g1->addEdge(0, 1); g2->addEdge(2, 3);
return 0;
}
Directed: 0 → 1
Undirected: 2 — 3Strategy Pattern
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class SortStrategy {
public:
virtual void sort(vector<int>& data) = 0;
virtual string name() const = 0;
virtual ~SortStrategy() = default;
};
class STLSort : public SortStrategy {
public:
void sort(vector<int>& data) override { std::sort(data.begin(), data.end()); }
string name() const override { return "std::sort (introsort)"; }
};
class BubbleSort : public SortStrategy {
public:
void sort(vector<int>& data) override {
int n = data.size();
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-i-1; j++)
if (data[j] > data[j+1]) swap(data[j], data[j+1]);
}
string name() const override { return "Bubble Sort O(n²)"; }
};
class Sorter {
SortStrategy* strategy;
public:
Sorter(SortStrategy* s) : strategy(s) {}
void setStrategy(SortStrategy* s) { strategy = s; }
void sort(vector<int>& data) { cout << "Using: " << strategy->name() << endl; strategy->sort(data); }
};
int main() {
vector<int> data = {5,2,8,1,9};
STLSort stl; BubbleSort bubble;
Sorter sorter(&stl);
sorter.sort(data);
for (int x : data) cout << x << " "; cout << endl;
data = {5,2,8,1,9};
sorter.setStrategy(&bubble);
sorter.sort(data);
for (int x : data) cout << x << " "; cout << endl;
return 0;
}
Using: std::sort (introsort)
1 2 5 8 9
Using: Bubble Sort O(n²)
1 2 5 8 9 DSA Application: Polymorphic Priority Queue with Operator Overloading
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
class IPriorityQueue {
public:
virtual void push(int v) = 0;
virtual int pop() = 0;
virtual bool isEmpty() const = 0;
virtual ~IPriorityQueue() = default;
};
class MaxHeap : public IPriorityQueue {
vector<int> h;
void siftUp(int i) { while(i>0&&h[(i-1)/2]<h[i]){swap(h[i],h[(i-1)/2]);i=(i-1)/2;} }
void siftDown(int i){int n=h.size(),mx=i;if(2*i+1<n&&h[2*i+1]>h[mx])mx=2*i+1;if(2*i+2<n&&h[2*i+2]>h[mx])mx=2*i+2;if(mx!=i){swap(h[i],h[mx]);siftDown(mx);}}
public:
void push(int v) override { h.push_back(v); siftUp(h.size()-1); }
int pop() override { if(h.empty())throw runtime_error("empty"); int r=h[0];h[0]=h.back();h.pop_back();if(!h.empty())siftDown(0);return r; }
bool isEmpty() const override { return h.empty(); }
MaxHeap operator+(const MaxHeap& o) const {
MaxHeap result; result.h=h;
result.h.insert(result.h.end(),o.h.begin(),o.h.end());
for(int i=result.h.size()/2-1;i>=0;--i) result.siftDown(i);
return result;
}
};
int main() {
MaxHeap pq1, pq2;
pq1.push(5); pq1.push(3); pq2.push(7); pq2.push(1);
MaxHeap merged = pq1 + pq2;
while(!merged.isEmpty()) cout << merged.pop() << " ";
cout << endl;
return 0;
}
7 5 3 1 Best Practices & Common Mistakes
✅ Virtual destructor in every base class
If your class has any virtual function, give it virtual ~Base() = default; to prevent resource leaks when deleting derived objects through base pointers.
✅ Overload operators intuitively
Only overload operators when the meaning is obvious. + for matrix addition = intuitive. If in doubt, use a named method.
✅ Use override keyword
Always write override when overriding virtual functions. It catches typos at compile time instead of silently creating a new function.
✅ Prefer virtual to RTTI
If you're writing dynamic_cast chains, consider adding a virtual method to the base class instead. RTTI is a last resort, not the default.
✅ Use make_unique with Factory
Return unique_ptr<Base> from factory functions. This automatically manages memory and communicates ownership clearly.
✅ Delete copy in Singleton
Explicitly delete the copy constructor and copy assignment in Singleton: = delete. Prevents accidental copies that break the single-instance contract.
❌ Don't overload without both == and <
For STL compatibility, if you overload <, also overload ==. Missing one causes unexpected behavior in sets, maps, and algorithms.
❌ Don't forget virtual base constructor
In virtual inheritance, the most-derived class must call the virtual base constructor explicitly. Forgetting this causes the virtual base to use its default constructor.
Interview Questions & Answers
Base* b = new Derived(); or Base& b = derived_obj;ostream/istream. (2) Symmetric binary operators where making it a member would privilege one operand. Public accessor methods are better for everything else.static_cast is a compile-time cast — it trusts the programmer and does no runtime check. dynamic_cast performs a runtime type check. For pointers, it returns nullptr on failure. For references, it throws std::bad_cast. Use dynamic_cast when you're not certain of the actual type.Frequently Asked Questions
< and == for custom types to make them work with STL containers. For example: bool operator<(const Node& o) const { return cost < o.cost; } makes priority_queue<Node> work correctly for Dijkstra's algorithm.interface keyword. C++ achieves the same with an abstract class that has only pure virtual functions and no data members. C++ also supports multiple inheritance from multiple abstract classes, while Java limits multiple interface implementation.Master C++ from scratch to advanced OOP
Structured lessons, 200+ exercises, and certificate of completion. Join 50,000+ students on CoodeVerse.