Module 07: Advanced OOP
Goal: Master advanced techniques — composition, CRTP, RAII, smart pointers, and modern C++ OOP idioms. Time: 2 days of focused study Prerequisites: Module 01-06
Table of Contents
- Composition over Inheritance
- CRTP — Curiously Recurring Template Pattern
- Abstract Classes vs Interfaces (Revisited)
- Dependency Injection in Practice
- RAII — Resource Acquisition Is Initialization
- Smart Pointers Deep Dive
- Type Erasure
- Mixins and Policy-Based Design
- Common OOP Design Mistakes
- Interview Questions
1. Composition over Inheritance
Why Prefer Composition?
INHERITANCE problems:
1. TIGHT COUPLING — derived class is bound to base class implementation
2. FRAGILE BASE CLASS — changing base can break all derived classes
3. RIGID HIERARCHY — can't change relationships at runtime
4. GOD CLASS risk — temptation to put everything in the base
COMPOSITION benefits:
1. LOOSE COUPLING — components are independent
2. FLEXIBLE — swap components at runtime
3. TESTABLE — mock individual components
4. REUSABLE — components can be used in different combinationsExample: Game Character
// ❌ INHERITANCE — Explosion of classes
class Character {};
class Warrior : public Character {};
class FlyingWarrior : public Warrior {}; // Can fly + fight
class SwimmingWarrior : public Warrior {}; // Can swim + fight
class FlyingSwimmingWarrior : public ??? {}; // Nightmare!
// 3 abilities × combinations = exponential class explosion!
// ✅ COMPOSITION — Mix and match behaviors
class IMovement {
public:
virtual void move() = 0;
virtual ~IMovement() = default;
};
class Walking : public IMovement {
public: void move() override { cout << "Walking" << endl; }
};
class Flying : public IMovement {
public: void move() override { cout << "Flying" << endl; }
};
class Swimming : public IMovement {
public: void move() override { cout << "Swimming" << endl; }
};
class IAttack {
public:
virtual void attack() = 0;
virtual ~IAttack() = default;
};
class Melee : public IAttack {
public: void attack() override { cout << "Sword slash!" << endl; }
};
class Ranged : public IAttack {
public: void attack() override { cout << "Arrow shot!" << endl; }
};
class Magic : public IAttack {
public: void attack() override { cout << "Fireball!" << endl; }
};
// Character is COMPOSED of behaviors
class Character {
string name;
unique_ptr<IMovement> movement;
unique_ptr<IAttack> attackStyle;
public:
Character(string n, unique_ptr<IMovement> m, unique_ptr<IAttack> a)
: name(n), movement(move(m)), attackStyle(move(a)) {}
void move() { movement->move(); }
void attack() { attackStyle->attack(); }
// Change behavior at RUNTIME!
void setMovement(unique_ptr<IMovement> m) { movement = move(m); }
void setAttack(unique_ptr<IAttack> a) { attackStyle = move(a); }
};
int main() {
auto hero = Character("Hero",
make_unique<Walking>(),
make_unique<Melee>());
hero.move(); // Walking
hero.attack(); // Sword slash!
// Power-up! Now can fly and use magic
hero.setMovement(make_unique<Flying>());
hero.setAttack(make_unique<Magic>());
hero.move(); // Flying
hero.attack(); // Fireball!
// No new classes needed! Just swap components.
}When to Still Use Inheritance
USE INHERITANCE when:
✅ True "IS-A" relationship that won't change
✅ You need polymorphism (virtual functions, base pointers)
✅ The base class defines a stable, well-designed interface
✅ You're modeling a well-understood taxonomy
USE COMPOSITION when:
✅ "HAS-A" relationship
✅ Behavior should be interchangeable
✅ You want to avoid tight coupling
✅ Multiple inheritance seems tempting
✅ You need runtime flexibility
✅ When in doubt!2. CRTP
CRTP = a class inherits from a template that takes itself as a parameter. Enables static (compile-time) polymorphism.
// Base class is templated on the derived class
template <typename Derived>
class Counter {
static int count;
public:
Counter() { count++; }
~Counter() { count--; }
static int getCount() { return count; }
};
template <typename Derived>
int Counter<Derived>::count = 0;
// Each derived class gets its OWN counter!
class Dog : public Counter<Dog> {};
class Cat : public Counter<Cat> {};
int main() {
Dog d1, d2, d3;
Cat c1, c2;
cout << Dog::getCount() << endl; // 3
cout << Cat::getCount() << endl; // 2
// Each class has its own static count!
}Static Polymorphism with CRTP
// No virtual functions, no vtable, no runtime overhead!
template <typename Derived>
class Shape {
public:
double area() const {
return static_cast<const Derived*>(this)->area_impl();
}
void draw() const {
static_cast<const Derived*>(this)->draw_impl();
}
};
class Circle : public Shape<Circle> {
double radius;
public:
Circle(double r) : radius(r) {}
double area_impl() const { return 3.14159 * radius * radius; }
void draw_impl() const { cout << "Drawing circle, r=" << radius << endl; }
};
class Rect : public Shape<Rect> {
double w, h;
public:
Rect(double w, double h) : w(w), h(h) {}
double area_impl() const { return w * h; }
void draw_impl() const { cout << "Drawing rect " << w << "x" << h << endl; }
};
// CRTP enables this at compile time — no virtual dispatch!
template <typename T>
void printArea(const Shape<T>& shape) {
cout << "Area: " << shape.area() << endl; // No vtable lookup!
}
int main() {
Circle c(5);
Rect r(3, 4);
printArea(c); // Area: 78.5398
printArea(r); // Area: 12
}
/*
CRTP vs Virtual Functions:
Virtual: Runtime dispatch, ~2 memory accesses overhead, flexible
CRTP: Compile-time, zero overhead (inlined), but no heterogeneous collections
Can't do: vector<Shape*> with CRTP (no common base)
Can do: vector<Shape*> with virtual functions
*/3. Abstract Classes vs Interfaces
Feature │ Abstract Class │ Interface (Pure Abstract)
─────────────────────┼──────────────────────────┼─────────────────────────
Pure virtual funcs │ At least one │ ALL functions
Data members │ ✅ Can have │ ❌ None (by convention)
Constructors │ ✅ Can have │ ❌ None (usually)
Method bodies │ ✅ Some implemented │ ❌ None
Multiple inheritance │ ⚠️ Diamond problem risk │ ✅ Safe (no data)
Purpose │ Share implementation │ Define contract
When to use │ Common code + specialization│ Unrelated classes, same behavior// ABSTRACT CLASS — shares code among related classes
class Animal {
protected:
string name;
int health;
public:
Animal(string n, int h) : name(n), health(h) {}
// Shared implementation
void eat() { health += 10; }
string getName() const { return name; }
// Must be specialized
virtual void speak() const = 0;
virtual ~Animal() = default;
};
// INTERFACE — defines a contract for unrelated classes
class ISerializable {
public:
virtual string serialize() const = 0;
virtual void deserialize(const string& data) = 0;
virtual ~ISerializable() = default;
};
// A Dog IS-A Animal (abstract class) and CAN BE serialized (interface)
class Dog : public Animal, public ISerializable {
public:
Dog(string n) : Animal(n, 100) {}
void speak() const override { cout << "Woof!" << endl; }
string serialize() const override { return name + ":" + to_string(health); }
void deserialize(const string& data) override { /* parse */ }
};
// Config is NOT an animal but CAN BE serialized
class Config : public ISerializable {
map<string, string> settings;
public:
string serialize() const override { /* ... */ return ""; }
void deserialize(const string& data) override { /* ... */ }
};4. Dependency Injection
// Full DI example with constructor injection
// Interfaces
class ILogger {
public:
virtual void log(const string& msg) = 0;
virtual ~ILogger() = default;
};
class IUserRepository {
public:
virtual void save(const string& user) = 0;
virtual string findById(int id) = 0;
virtual ~IUserRepository() = default;
};
class IEmailService {
public:
virtual void send(const string& to, const string& body) = 0;
virtual ~IEmailService() = default;
};
// Implementations
class ConsoleLogger : public ILogger {
public:
void log(const string& msg) override { cout << "[LOG] " << msg << endl; }
};
class DatabaseRepo : public IUserRepository {
public:
void save(const string& user) override { cout << "DB: Saved " << user << endl; }
string findById(int id) override { return "User#" + to_string(id); }
};
class SMTPEmail : public IEmailService {
public:
void send(const string& to, const string& body) override {
cout << "Email to " << to << ": " << body << endl;
}
};
// Service with ALL dependencies injected
class UserService {
IUserRepository& repo;
ILogger& logger;
IEmailService& email;
public:
UserService(IUserRepository& r, ILogger& l, IEmailService& e)
: repo(r), logger(l), email(e) {}
void registerUser(const string& name, const string& emailAddr) {
logger.log("Registering user: " + name);
repo.save(name);
email.send(emailAddr, "Welcome, " + name + "!");
logger.log("User registered successfully");
}
};
// ---- TESTING: Inject mocks! ----
class MockLogger : public ILogger {
public:
vector<string> logs;
void log(const string& msg) override { logs.push_back(msg); }
};
class MockRepo : public IUserRepository {
public:
vector<string> saved;
void save(const string& user) override { saved.push_back(user); }
string findById(int id) override { return "MockUser"; }
};
class MockEmail : public IEmailService {
public:
int sendCount = 0;
void send(const string& to, const string& body) override { sendCount++; }
};
int main() {
// Production
ConsoleLogger logger;
DatabaseRepo repo;
SMTPEmail email;
UserService service(repo, logger, email);
service.registerUser("Alice", "alice@test.com");
// Testing — swap real dependencies with mocks!
MockLogger mockLog;
MockRepo mockRepo;
MockEmail mockEmail;
UserService testService(mockRepo, mockLog, mockEmail);
testService.registerUser("TestUser", "test@test.com");
// Verify behavior
assert(mockRepo.saved.size() == 1);
assert(mockEmail.sendCount == 1);
assert(mockLog.logs.size() == 2);
}5. RAII
RAII = Resource Acquisition Is Initialization. Tie resource lifetime to object lifetime.
// The resource is acquired in the constructor
// and released in the destructor.
// Since destructors run automatically, resources can't leak!
// ---- FILE HANDLE ----
class FileGuard {
FILE* file;
public:
FileGuard(const string& name, const string& mode) {
file = fopen(name.c_str(), mode.c_str());
if (!file) throw runtime_error("Can't open file: " + name);
}
~FileGuard() {
if (file) fclose(file); // ALWAYS closed, even if exception thrown!
}
// Delete copy (file handles aren't copyable)
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;
FILE* get() { return file; }
};
// ---- MUTEX LOCK ----
class LockGuard {
mutex& mtx;
public:
LockGuard(mutex& m) : mtx(m) { mtx.lock(); }
~LockGuard() { mtx.unlock(); } // ALWAYS unlocked!
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
};
// Usage:
void processFile(const string& name) {
FileGuard file(name, "r"); // Opens file
// Do work...
// If exception is thrown here, file is STILL closed!
} // file goes out of scope → destructor closes it
void threadSafeUpdate(mutex& mtx, int& value) {
LockGuard lock(mtx); // Acquires lock
value++;
// If exception is thrown here, lock is STILL released!
} // lock goes out of scope → destructor unlocks
// Standard library RAII types:
// std::unique_ptr, std::shared_ptr — memory
// std::lock_guard, std::unique_lock — mutexes
// std::fstream — files
// std::jthread — threads (C++20)6. Smart Pointers
unique_ptr — Exclusive Ownership
#include <memory>
class Resource {
public:
Resource(int id) { cout << "Resource " << id << " created" << endl; }
~Resource() { cout << "Resource destroyed" << endl; }
void use() { cout << "Using resource" << endl; }
};
int main() {
// Create
auto r1 = make_unique<Resource>(1);
r1->use();
// Transfer ownership (move-only, can't copy!)
auto r2 = move(r1);
// r1 is now nullptr!
// r2 owns the resource
r2->use();
// For arrays:
auto arr = make_unique<int[]>(10);
arr[0] = 42;
} // r2 goes out of scope → Resource automatically destroyed
// unique_ptr overhead: ZERO! Same size as raw pointer.shared_ptr — Shared Ownership
int main() {
shared_ptr<Resource> r1 = make_shared<Resource>(1);
cout << "Count: " << r1.use_count() << endl; // 1
{
shared_ptr<Resource> r2 = r1; // Share ownership
cout << "Count: " << r1.use_count() << endl; // 2
shared_ptr<Resource> r3 = r1; // Share again
cout << "Count: " << r1.use_count() << endl; // 3
} // r2, r3 destroyed → count drops to 1
cout << "Count: " << r1.use_count() << endl; // 1
} // r1 destroyed → count reaches 0 → Resource destroyed
// shared_ptr overhead:
// - Extra memory: control block (reference count + weak count + deleter)
// - Thread-safe reference counting (atomic operations)
// - Typically 2x the size of a raw pointerweak_ptr — Non-Owning Observer (Breaks Circular References)
class Node {
public:
string name;
shared_ptr<Node> next;
// weak_ptr<Node> prev; // Use weak_ptr to break cycles!
Node(string n) : name(n) { cout << name << " created" << endl; }
~Node() { cout << name << " destroyed" << endl; }
};
int main() {
// ❌ CIRCULAR REFERENCE with shared_ptr — MEMORY LEAK!
auto a = make_shared<Node>("A");
auto b = make_shared<Node>("B");
a->next = b; // A → B
b->next = a; // B → A ← CYCLE! Neither can reach count 0!
// A and B are NEVER destroyed — memory leak!
// ✅ Break cycle with weak_ptr
// Change: shared_ptr<Node> prev → weak_ptr<Node> prev
// weak_ptr doesn't increase reference count
}
// Using weak_ptr:
void example() {
auto shared = make_shared<Resource>(1);
weak_ptr<Resource> weak = shared;
// Check if resource still exists
if (auto locked = weak.lock()) { // Returns shared_ptr if alive
locked->use();
}
shared.reset(); // Destroy resource
if (weak.expired()) {
cout << "Resource is gone!" << endl;
}
}Which Smart Pointer to Use
unique_ptr:
✅ Default choice — use unless you need sharing
✅ Zero overhead
✅ Clear ownership (one owner)
shared_ptr:
✅ Multiple owners need to share an object
✅ Observer pattern, caches
⚠️ Higher overhead (reference counting)
⚠️ Watch for circular references
weak_ptr:
✅ Observing a shared_ptr without owning it
✅ Breaking circular references
✅ Cache invalidation
raw pointer:
✅ Non-owning reference to an existing object
✅ When smart pointers add unnecessary overhead
❌ NEVER use for ownership!7. Type Erasure
Making different unrelated types usable through a uniform interface without inheritance.
#include <functional>
// std::function is the canonical example of type erasure
// It can hold ANY callable: function, lambda, functor, member function
void freeFunc(int x) { cout << "Free: " << x << endl; }
class Functor {
public:
void operator()(int x) { cout << "Functor: " << x << endl; }
};
int main() {
// All these different types erased to std::function<void(int)>
function<void(int)> f;
f = freeFunc; // Free function
f(1);
f = Functor(); // Functor object
f(2);
f = [](int x) { cout << "Lambda: " << x << endl; }; // Lambda
f(3);
// Store different callables in a vector!
vector<function<void(int)>> handlers;
handlers.push_back(freeFunc);
handlers.push_back(Functor());
handlers.push_back([](int x) { cout << "Lambda: " << x << endl; });
for (auto& handler : handlers) {
handler(42); // Calls each one through the same interface
}
}8. Mixins and Policy-Based Design
Mixins via CRTP
// Add serialization capability to any class
template <typename Derived>
class Serializable {
public:
string toJSON() const {
return static_cast<const Derived*>(this)->toJSON_impl();
}
};
// Add logging capability to any class
template <typename Derived>
class Loggable {
public:
void log(const string& msg) const {
cout << "[" << typeid(Derived).name() << "] " << msg << endl;
}
};
// Mix in capabilities!
class User : public Serializable<User>, public Loggable<User> {
string name;
int age;
public:
User(string n, int a) : name(n), age(a) {}
string toJSON_impl() const {
return R"({"name":")" + name + R"(","age":)" + to_string(age) + "}";
}
};
int main() {
User u("Alice", 25);
cout << u.toJSON() << endl; // {"name":"Alice","age":25}
u.log("User created"); // [User] User created
}Policy-Based Design
// Policies = template parameters that define behavior
// Locking policies
struct NoLock {
void lock() {}
void unlock() {}
};
struct MutexLock {
mutex mtx;
void lock() { mtx.lock(); }
void unlock() { mtx.unlock(); }
};
// Storage policies
template <typename T>
struct VectorStorage {
vector<T> data;
void add(const T& val) { data.push_back(val); }
size_t count() const { return data.size(); }
};
template <typename T>
struct ListStorage {
list<T> data;
void add(const T& val) { data.push_back(val); }
size_t count() const { return data.size(); }
};
// Container composed of policies
template <typename T,
template<typename> class Storage = VectorStorage,
typename LockPolicy = NoLock>
class Container : private Storage<T>, private LockPolicy {
public:
void add(const T& val) {
LockPolicy::lock();
Storage<T>::add(val);
LockPolicy::unlock();
}
size_t size() const { return Storage<T>::count(); }
};
// Mix and match at compile time!
Container<int> simple; // Vector, no lock
Container<int, ListStorage> listBased; // List, no lock
Container<int, VectorStorage, MutexLock> threadSafe; // Vector, mutex9. Common Design Mistakes
1. OVER-ENGINEERING
Don't create interfaces/abstractions until you need them.
"You Aren't Gonna Need It" (YAGNI)
2. INHERITANCE FOR CODE REUSE ONLY
Just because two classes share code doesn't mean one should inherit from the other.
Use composition or utility classes instead.
3. GOD OBJECTS
One class that knows and does everything. Split into focused classes.
4. PREMATURE ABSTRACTION
Creating abstract base classes before you have more than one implementation.
5. IGNORING OWNERSHIP SEMANTICS
Using raw pointers everywhere instead of clearly indicating ownership
with unique_ptr (owning) and raw pointers/references (non-owning).
6. MAKING EVERYTHING VIRTUAL
Virtual functions have overhead and indicate extension points.
Only make functions virtual when polymorphism is intended.10. Interview Questions
Q1: Why "prefer composition over inheritance"?
Answer: Inheritance creates tight coupling — the derived class depends on base class implementation details. Composition is more flexible: components can be swapped at runtime, tested independently, and reused across unrelated classes. Inheritance should be reserved for true "IS-A" relationships where polymorphism is needed. Composition avoids fragile base class problem and class explosion.
Q2: What is RAII? Why is it important in C++?
Answer: RAII ties resource lifetime to object lifetime. Resources (memory, files, locks, connections) are acquired in constructors and released in destructors. Since C++ guarantees destructors run when objects go out of scope (even during exceptions), RAII prevents resource leaks. Examples: unique_ptr (memory), lock_guard (mutex), fstream (files). It's C++'s primary mechanism for exception-safe resource management.
Q3: Explain unique_ptr vs shared_ptr vs weak_ptr.
Answer: unique_ptr: exclusive ownership, zero overhead, move-only (can't copy). Default choice. shared_ptr: shared ownership via reference counting, thread-safe, more overhead. Use when multiple owners genuinely need shared access. weak_ptr: non-owning observer of a shared_ptr, doesn't affect reference count, used to break circular references and for caches.
Q4: What is CRTP? How is it different from virtual functions?
Answer: CRTP is a pattern where a class inherits from a template parameterized on itself: class Dog : public Base<Dog>. It enables static (compile-time) polymorphism with zero runtime overhead — no vtable, calls are inlined. Virtual functions provide runtime polymorphism (can put different types in one container). CRTP: better performance, no heterogeneous containers. Virtual: flexible, supports runtime type decisions.
Q5: What is Dependency Injection? What are its benefits?
Answer: DI is passing dependencies into a class (via constructor, setter, or parameter) instead of creating them internally. Benefits: (1) Testable — inject mocks for testing. (2) Flexible — swap implementations without changing the class. (3) Decoupled — class doesn't know about concrete dependencies. (4) Follows DIP — depends on abstractions, not concrete types.
Q6: What is type erasure in C++?
Answer: Type erasure hides the concrete type behind a uniform interface without inheritance. std::function is the prime example — it can hold a free function, lambda, functor, or member function through one type. Internally it uses a combination of templates, virtual functions (or function pointers), and heap allocation. Benefits: store different callable types in one container without a common base class.
Q7: What are smart pointers? When would you still use raw pointers?
Answer: Smart pointers manage heap memory automatically via RAII. Use raw pointers for non-owning references (observing an object managed elsewhere), interfacing with C APIs, and when smart pointer overhead matters (rare). Never use raw pointers for ownership. Rule of thumb: unique_ptr for ownership, raw pointer or reference for observation.
Q8: When would you use CRTP vs virtual functions?
Answer: Use CRTP when you know all types at compile time and want zero-overhead polymorphism (compile-time dispatch). Use virtual functions when you need runtime polymorphism: heterogeneous containers, plugin architectures, or types determined at runtime. CRTP can't be used for vector<Base*> collections. Modern compilers can often devirtualize simple virtual calls, narrowing the gap.
Next Module: 08 - Real-World & Interview Prep — Full project walkthroughs, UML, and 100+ interview questions.