Overview
This lab builds a Shape class that declares an interface instead of a concrete implementation, then three classes, Circle, Rectangle and Triangle, that each implement it their own way. Starting from Shape's pure virtual area(), perimeter() and name(), you will write the three concrete shapes and then store all of them in a single std::vector<Shape*>, calling the same method on every element and watching each one run its own version. In the Inheritance lab, a non-virtual start() redeclared in Car hid Vehicle's version rather than replacing it. This lab shows the other half of that picture, a pure virtual method that is correctly dispatched to the derived class every time, no matter what type of pointer is used to call it.
Learning Objectives
By the end of this lab you will be able to:
- Declare an abstract base class using pure virtual functions
- Explain why a class with even one pure virtual function cannot be instantiated
- Implement a shared, non-virtual method that dispatches to derived overrides through the interface
- Give a base class a virtual destructor so deletion through a base pointer runs the correct derived destructor
- Store and process multiple derived types through a single std::vector
without knowing their concrete type
Concept
A game often needs one function that works across several unrelated types, drawing every shape on screen, updating every entity, saving every item, without a long chain of if statements checking what each one actually is. Adding a new type would mean finding and editing every one of those chains, and forgetting one is easy to miss until it fails at runtime.
C++ solves this with an abstract class. A class becomes abstract the moment it declares a pure virtual function, a method written as virtual returnType name() = 0 with no body at all. The = 0 tells the compiler this class only describes an interface, a set of methods every derived class must supply, and it refuses to let anyone construct the abstract class directly, since there would be nothing to run if one of those methods were ever called. A derived class only stops being abstract once it overrides every pure virtual function it inherited.
A base class can still supply real, shared behaviour alongside its pure virtuals. A plain (non-pure) method written once in the base class can call the pure virtual methods internally, and because those calls are resolved at runtime based on the actual object behind the pointer, the same base class code produces different output for every derived type without ever being duplicated. This only works safely alongside a virtual destructor, virtual ~Shape() {}, which makes sure that deleting a derived object through a Shape* still runs that derived class's own destructor.
Tasks
Task 1 - The Shape interface
Open Shape.h and Shape.cpp and complete the Shape class. It declares the interface every shape must provide but supplies no shape-specific data of its own.
- In
Shape.h, declare a virtual destructor with an empty body. - Declare three pure virtual methods,
area(),perimeter()andname(), each returning the type shown in the skeleton and taking no arguments. Give each one a= 0body so the class stays abstract. - Declare
describe()as a normal (non-pure)constmethod with no return value. - In
Shape.cpp, implementdescribe()to print the shape'sname(),area()andperimeter()tostd::cout, in that order, separated by": area = "and", perimeter = ".
Success condition: Shape.h and Shape.cpp compile cleanly on their own. If you temporarily add the line Shape shape (with a trailing semicolon) anywhere, the compiler rejects it because Shape is abstract, while a pointer declaration such as Shape* shapePtr compiles without a derived class in sight. You will get a concrete shape to test describe() against in Task 2.
#pragma once
// Shape is an abstract base class. It declares the interface every shape
// must provide but supplies no data of its own, so it can never be
// constructed directly.
class Shape
{
public:
// TODO: declare a virtual destructor with an empty body
// TODO: declare area(), perimeter() and name() as pure virtual (= 0),
// each const, taking no arguments. area() and perimeter() return
// float, name() returns const char*
// TODO: declare describe() as a normal const method with no return
// value (implemented in Shape.cpp, not here)
};
#include "Shape.h"
#include <iostream>
void Shape::describe() const
{
// TODO: print name(), area() and perimeter() to std::cout, formatted as
// "<name>: area = <area>, perimeter = <perimeter>" followed by std::endl
}
Task 2 - Circle, Rectangle and Triangle: three implementations of one interface
Open Circle.h, Circle.cpp, Rectangle.h and Rectangle.cpp, and complete each class so it implements the Shape interface.
- In each header, derive the class publicly from
Shapeand declare its private state, aradiusforCircle, awidthandheightforRectangle. - Declare a constructor taking that state as parameters, and declare overrides of
area(),perimeter()andname(). - In each source file, implement the constructor by assigning every parameter to its matching member using
this->. - Implement
area()andperimeter()using the standard formulas for that shape, andname()to return the class's own name as a string literal.
Now open Triangle.h and Triangle.cpp and repeat the same pattern, but notice Triangle needs more state than the other two, a base and height for its area, plus sideA and sideB alongside base for its perimeter. Shape does not care how much state a derived class needs. it only requires that area(), perimeter() and name() exist.
Success condition: Circle.h/.cpp, Rectangle.h/.cpp and Triangle.h/.cpp all compile together with Shape.h. Task 3 assembles all three into a program that prints one line of output per shape to confirm each override is correct.
#pragma once
#include "Shape.h"
class Circle // TODO: derive publicly from Shape
{
private:
// TODO: declare a private float member, radius
public:
Circle(float radius);
// TODO: declare overrides of area(), perimeter() and name()
};
#include "Circle.h"
const float PI = 3.14159265f;
Circle::Circle(float radius)
{
// TODO: assign the parameter to the radius member using this->
}
float Circle::area() const
{
// TODO: return PI * radius * radius
}
float Circle::perimeter() const
{
// TODO: return 2.0f * PI * radius
}
const char* Circle::name() const
{
// TODO: return "Circle"
}
#pragma once
#include "Shape.h"
class Rectangle // TODO: derive publicly from Shape
{
private:
// TODO: declare two private float members, width and height
public:
Rectangle(float width, float height);
// TODO: declare overrides of area(), perimeter() and name()
};
#include "Rectangle.h"
Rectangle::Rectangle(float width, float height)
{
// TODO: assign each parameter to its matching member using this->
}
float Rectangle::area() const
{
// TODO: return width * height
}
float Rectangle::perimeter() const
{
// TODO: return 2.0f * (width + height)
}
const char* Rectangle::name() const
{
// TODO: return "Rectangle"
}
#pragma once
#include "Shape.h"
// Triangle uses base and height for area, but all three sides for
// perimeter, so it needs more state than Circle or Rectangle to implement
// the same Shape interface.
class Triangle // TODO: derive publicly from Shape
{
private:
// TODO: declare four private float members: base, height, sideA, sideB
public:
Triangle(float base, float height, float sideA, float sideB);
// TODO: declare overrides of area(), perimeter() and name()
};
#include "Triangle.h"
Triangle::Triangle(float base, float height, float sideA, float sideB)
{
// TODO: assign each parameter to its matching member using this->
}
float Triangle::area() const
{
// TODO: return 0.5f * base * height
}
float Triangle::perimeter() const
{
// TODO: return base + sideA + sideB
}
const char* Triangle::name() const
{
// TODO: return "Triangle"
}
Task 3 - Polymorphic dispatch through a vector
Open AbstractionInterfacesDemo.cpp and complete main() so it exercises all three shapes through the Shape interface alone.
- Declare a
std::vector<Shape*>namedshapes. - Push back a
new Circle(3.0f), anew Rectangle(4.0f, 5.0f)and anew Triangle(6.0f, 4.0f, 5.0f, 5.0f), in that order. - Loop over
shapeswith a range-basedfor (Shape* shape : shapes)and callshape->describe()on each one. - Loop over
shapesa second time anddeleteeach pointer.
Success condition: running the program prints exactly this, with the same numbers for every run:
Circle: area = 28.274, perimeter = 18.8496
Rectangle: area = 20, perimeter = 18
Triangle: area = 12, perimeter = 16
Every line comes from the same call, shape->describe(), through the same Shape* variable. describe() itself is written once in Shape.cpp and never changes, yet its output is different for every element because name(), area() and perimeter() resolve to whichever derived class the pointer actually points to. Deleting through Shape* still runs each derived class's own destructor correctly, because Shape's destructor is virtual.
#include <iostream>
#include <vector>
#include "Shape.h"
#include "Circle.h"
#include "Rectangle.h"
#include "Triangle.h"
int main()
{
// Shape shape; // error: cannot instantiate abstract class "Shape"
// TODO: declare a std::vector<Shape*> named shapes
// TODO: push back a new Circle(3.0f), a new Rectangle(4.0f, 5.0f) and a
// new Triangle(6.0f, 4.0f, 5.0f, 5.0f), in that order
// TODO: loop over shapes with a range-based for (Shape* shape : shapes)
// and call shape->describe() on each one
// TODO: loop over shapes again and delete each pointer
}
Solution
Try the tasks above before revealing the solution.