Skip to content

C++ Interfaces

In object-oriented programming, an interface defines a contract for a class. It specifies what a class can do (its capabilities or methods) without dictating how it does it. This promotes loose coupling and polymorphism.

C++ implements interfaces using abstract classes. An abstract class is a class that cannot be instantiated directly and is intended to be used as a base class for derived classes.

A class becomes abstract if it declares at least one pure virtual function. A pure virtual function is a virtual function that is declared but not necessarily defined in the base class. It’s declared by appending = 0 to its signature:

#include <string>
// Abstract base class defining an 'Drawable' interface
class Drawable {
public:
// Pure virtual function - derived classes MUST implement this
virtual void draw() const = 0;
// Abstract classes can still have regular members and methods
virtual std::string getName() const { return "Generic Drawable"; }
// IMPORTANT: Abstract base classes used polymorphically need a virtual destructor
virtual ~Drawable() = default; // Use default implementation
};

Key points about abstract classes:

  • Cannot Instantiate: You cannot create objects of an abstract class type directly (Drawable d; would cause a compile error).
  • Interface Role: They serve primarily as interfaces, defining a common set of functions that derived classes must implement.
  • Polymorphism: They are typically used with pointers or references to allow treating objects of different derived classes through the common base class interface.
  • Mandatory Implementation: Any concrete (non-abstract) class deriving from an abstract class must provide implementations for all inherited pure virtual functions.
  • Virtual Destructor: If you intend to delete derived objects through a base class pointer (Drawable* ptr = new Circle(); delete ptr;), the base class must have a virtual destructor. Otherwise, only the base class destructor runs, leading to resource leaks (Undefined Behavior). Adding virtual ~ClassName() = default; is often sufficient.

A class that can be instantiated (i.e., is not abstract) is called a concrete class. A concrete class derived from an abstract class provides implementations for all inherited pure virtual functions.

Let’s implement the Drawable interface with concrete Circle and Square classes:

#include <iostream>
#include <string>
#include <vector>
#include <memory> // For std::unique_ptr
// Abstract base class (Interface)
class Shape {
public:
// Pure virtual function - defines the interface contract
virtual double getArea() const = 0;
virtual std::string getName() const = 0;
// Virtual destructor is crucial for base classes with virtual functions
virtual ~Shape() { std::cout << "Destroying Shape\n"; }; // Example non-default destructor
// Common utility function (optional)
void displayInfo() const {
std::cout << "Shape Type: " << getName() << ", Area: " << getArea() << std::endl;
}
protected:
// Protected constructor if needed, but Shape cannot be instantiated directly
Shape() = default;
};
// Concrete derived class: Rectangle
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
// Override pure virtual functions
double getArea() const override {
return width * height;
}
std::string getName() const override {
return "Rectangle";
}
~Rectangle() override { std::cout << "Destroying Rectangle\n"; }
private:
double width;
double height;
};
// Concrete derived class: Triangle
class Triangle : public Shape {
public:
Triangle(double b, double h) : base(b), height(h) {}
// Override pure virtual functions
double getArea() const override {
return 0.5 * base * height;
}
std::string getName() const override {
return "Triangle";
}
~Triangle() override { std::cout << "Destroying Triangle\n"; }
private:
double base;
double height;
};
int main() {
// Using unique_ptr for automatic memory management (RAII)
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Rectangle>(5.0, 7.0));
shapes.push_back(std::make_unique<Triangle>(5.0, 7.0));
std::cout << "Processing shapes polymorphically:\n";
for (const auto& shapePtr : shapes) {
// Calls the correct getArea/getName based on the actual object type
shapePtr->displayInfo();
}
std::cout << "\nShapes vector going out of scope...\n";
// unique_ptr automatically calls delete on the contained pointers,
// invoking the correct derived destructors thanks to the virtual base destructor.
return 0;
}

When the above code is compiled and executed, it produces output similar to:

Processing shapes polymorphically:
Shape Type: Rectangle, Area: 35
Shape Type: Triangle, Area: 17.5
Shapes vector going out of scope...
Destroying Rectangle
Destroying Shape
Destroying Triangle
Destroying Shape

Notice how we can treat Rectangle and Triangle objects uniformly through the Shape pointer. The override keyword helps ensure we are correctly implementing the virtual functions from the base class. The virtual destructor ensures proper cleanup.

Using abstract classes (interfaces) is a powerful design strategy:

  • Decoupling: Code that uses the interface (like the main function using Shape*) doesn’t need to know the specific concrete types (Rectangle, Triangle). It only depends on the Shape interface.
  • Extensibility: You can easily add new shapes (e.g., Circle) by deriving from Shape and implementing the required functions, without modifying the code that uses the Shape interface.
  • Standardization: Ensures that all derived classes provide a minimum set of functionalities defined by the pure virtual functions in the base class.

Modern C++ (C++20) also introduced Concepts, which provide a way to specify requirements on template arguments at compile time, sometimes serving as an alternative or complement to traditional interfaces for generic programming.