38  Polymorphism, Inheritance, and C++ OOP Deep Dive

39 Polymorphism, Inheritance, and C++ OOP Deep Dive

Week 8, Session 2. CSS 343.

39.1 Warmup

Write operator== for a Point class with x and y. Make it a member function.

class Point {
 public:
  Point(int x, int y) : x_(x), y_(y) {}
  bool operator==(const Point& other) const {
    return x_ == other.x_ && y_ == other.y_;
  }
 private:
  int x_, y_;
};

39.2 Learning objectives

  1. Explain how the vtable enables runtime polymorphism.
  2. Write an abstract base class with pure virtual methods and a concrete derived class.
  3. Describe the diamond problem and the virtual inheritance fix.
  4. Apply RAII by wrapping raw pointers in unique_ptr.
  5. Implement operator<< for a custom class as a friend non-member.

39.3 Inheritance — is-a

class Animal {
 public:
  virtual void speak() const { cout << "..." << endl; }
  virtual ~Animal() = default;
};

class Dog : public Animal {
 public:
  void speak() const override { cout << "Woof!" << endl; }
};

class GuideDog : public Dog {
 public:
  void speak() const override { cout << "Quiet woof." << endl; }
};

A GuideDog is a Dog is an Animal. Inheritance models specialization.

39.4 Virtual functions and the vtable

Animal* a = new Dog();
a->speak();           // prints "Woof!"

Without virtual on speak, this would print "..." (the Animal version). With virtual, the runtime looks up speak in the vtable — a per-class table of function pointers built by the compiler. Each object has a hidden pointer to its class’s vtable.

When you call a->speak():

  1. Follow a to the object.
  2. Read the object’s vtable pointer.
  3. Index into the vtable to find speak.
  4. Call that function.

One extra indirection. Pennies of overhead. Enables dynamic dispatch.

Always make destructors virtual in base classes that you delete polymorphically. Otherwise delete a; where a is Animal* pointing to a Dog will only call Animal’s destructor — Dog’s cleanup is skipped.

39.5 Pure virtual and abstract classes

class Shape {
 public:
  virtual double area() const = 0;    // pure virtual: no body, mandatory override
  virtual ~Shape() = default;
};

class Circle : public Shape {
 public:
  Circle(double r) : r_(r) {}
  double area() const override { return 3.14159 * r_ * r_; }
 private:
  double r_;
};

A class with at least one pure virtual function is abstract — you cannot instantiate it directly:

Shape s;          // ERROR: abstract class
Shape* s = new Circle(1.0);    // OK

Abstract base classes are how C++ does “interfaces” — they declare the contract without providing implementation.

39.6 Polymorphic container

vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(1.0));
shapes.push_back(make_unique<Square>(2.0));

double totalArea = 0;
for (const auto& s : shapes) totalArea += s->area();

The vector holds pointers (smart pointers, here), and each s->area() dispatches to the correct override.

39.7 The diamond problem

class A { /* ... */ };
class B : public A { /* ... */ };
class C : public A { /* ... */ };
class D : public B, public C { /* ... */ };

D inherits from both B and C, which both inherit from A. Without virtual, a D object has two A subobjects — one inherited via B, one via C. Calling D::someAMethod() is ambiguous.

Fix with virtual inheritance:

class B : virtual public A { /* ... */ };
class C : virtual public A { /* ... */ };
class D : public B, public C { /* ... */ };

Now D has exactly one A. (At slight runtime cost.)

In practice: avoid multiple inheritance from non-interface classes. Inherit from at most one concrete class plus interfaces. The C++ community has largely moved away from full multiple inheritance.

39.8 RAII and smart pointers

RAII = Resource Acquisition Is Initialization. The idea: tie resource lifetime to object lifetime. When the object goes out of scope (stack unwind), the destructor releases the resource.

This is exactly the Rule of Three pattern from 342, generalized. File handles, network connections, mutexes — all should be RAII-wrapped.

Modern C++ provides smart pointers in <memory>:

39.8.1 std::unique_ptr<T>

Single owner. Cannot be copied; can be moved.

unique_ptr<int> p = make_unique<int>(42);
cout << *p << endl;       // 42
// no delete needed — destructor frees when p goes out of scope

Use unique_ptr for almost all heap allocations in new C++ code.

39.8.2 std::shared_ptr<T>

Reference-counted shared ownership. Last one out frees.

shared_ptr<int> a = make_shared<int>(42);
shared_ptr<int> b = a;        // ref count = 2
// when both a and b go out of scope, free

Use when ownership is genuinely shared (a shared cache, a parent-child structure with cycle-broken weak_ptrs).

The new/delete you learned in 342 chapter 4 is still valid C++. It is also the source of 80% of memory bugs. Modern C++ practice: use smart pointers for ownership; use raw pointers only for non-owning observation. By the time you leave 343 you should have zero delete in your code.

39.9 Operator overloading reminder

Same rules as 342 chapter 6. Member or non-member; friends for operator<<. The Rule of Three becomes the Rule of Five when you add move constructors. We will not require move semantics in this book — just know they exist.

39.10 Try it

Build a Shape hierarchy with Circle, Rectangle, Triangle. Add a print virtual that outputs the shape’s name and area. Hold them in vector<unique_ptr<Shape>>. Iterate and call print on each.

Confirm with AddressSanitizer that nothing leaks.

39.11 Self-check

1. Why must a base class destructor be virtual when used polymorphically?
b. Without virtual, only the static (declared) type's destructor runs.
2. Default ownership smart pointer in modern C++ is:
c. unique_ptr is the cheapest single-owner. shared_ptr only when ownership is actually shared. auto_ptr is deprecated.

39.12 Challenges

  1. Implement the Shape hierarchy from “Try it” — and add a copy constructor (or clone() virtual) for safe duplication.
  2. Use shared_ptr to build a tree where children also point back to parents using weak_ptr (avoids cycles).
  3. Take a class from earlier in the course that uses new/delete and convert to smart pointers. Confirm zero leaks.

39.13 Where this fits

OOP done. We close 343 with a tour through theory of computation — finite automata and Turing machines — to see what cannot be solved, no matter how cleverly we design our objects.

You are here Coming up
Virtual functions, vtable, abstract classes, RAII, smart pointers Chapter 37: regex and finite automata