Skip to Content
Course content

42: Practice Exercise: Building a Class-Based Shape Hierarchy

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing junior code, and there is one pattern that pops up almost every single time someone first tackles a class hierarchy: the attempt to store derived objects in a container of base objects by value. It usually looks something like this:

std::vector<Shape> shapes;
shapes.push_back(Circle(5.0)); // Wait, where did my radius go?

The Myth of the Magic Vector

The misconception here is that C++ is "smart" enough to know that a Circle is a Shape and will somehow preserve the Circle's identity inside that vector. It won't. What's actually happening is called object slicing.

When you push a Circle into a vector<Shape>, the compiler allocates exactly enough space for a Shape object. It then copies only the Shape part of your Circle into that slot, effectively "slicing off" everything that makes it a circle—the radius, the specific area() override, all of it. Even if your area() function is marked virtual, it doesn't matter because the object in the vector is no longer a Circle; it's just a Shape.

To get actual polymorphism, you have to store pointers or references. I almost always recommend std::unique_ptr<Shape> these days. It tells the compiler: "I don't know exactly which shape this is, but it's located somewhere in memory, and I want to interact with it through the Shape interface."

The Virtual Destructor Safety Net

Once you move to pointers, you'll hit a second common trap. If you have a base class Shape and a derived class Circle, and you delete a Circle object via a Shape* pointer, C++ will only call the Shape destructor unless you explicitly tell it not to.

If your Circle class allocated memory for a label or a texture, that memory is now leaked. It's a silent killer. Always, without exception, make your base class destructor virtual:

class Shape {
public:
    virtual ~Shape() = default; // This ensures derived destructors are called
    virtual double area() const = 0;
};

Designing for Extensibility

When building your hierarchy, aim for the "Open-Closed Principle." Your Shape base class should be open for extension (you can add Triangle, Pentagon, etc.) but closed for modification (you shouldn't have to change the Shape class every time you add a new shape).

This is achieved by using pure virtual functions (the = 0 syntax). By doing this, you're creating a contract. You're saying: "I don't know how to calculate the area of a generic shape, but I guarantee that any class claiming to be a Shape will know how to do it." This forces derived classes to implement the logic, or they simply won't compile.




📋 Practical Task

Implementation Project: The Polymorphic Area Calculator

Your task is to build a small system that can calculate the total area of a collection of different geometric shapes without the main logic knowing which specific shapes are being used.

Requirements:

  • Create an abstract base class called Shape with a pure virtual function double area() const and a virtual destructor.
  • Implement three derived classes: Circle (requires radius), Rectangle (requires width and height), and Triangle (requires base and height).
  • In your main() function, create a std::vector of std::unique_ptr<Shape>.
  • Populate the vector with at least one of each shape type using std::make_unique.
  • Iterate through the vector using a loop and calculate the sum of all areas.
  • Print the total area to the console.

Constraints: Do not use a switch statement or if/else blocks to check the type of the shape (no dynamic_cast). Rely entirely on the virtual function call to handle the calculation.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.