Sometimes, it is useful to list a function in a base class even if there is no way to implement it there. Consider a set of classes representing different geometric objectsβCircle, Rectangle, etc. We would like to unite them under a single base class so that we can define common behaviors and write code that works with any possible GeometricObject.
One thing we know every GeometricObject should be able to do is .getArea(). For mysteryObject.getArea() to work, we need to declare getArea() in GeometricObject and then override it in the derived classes. But what should the base version do? How can we compute the area of a geometric object with no other information? We could throw an exception or return a default value, indicating that the area cannot be computed without specific details about the shape. But another approach is to make the function abstract .
An abstract method (also known as a pure virtual function in C++) is a method that is declared without an implementation. It is essentially a promise that every GeometricObject subclass will provide its own implementation of the method, even though GeometricObject itself does not. In UML, we typically indicate abstract methods with italics:
Circle and Rectangle both inherit from GeometricObject
classDiagram
class Circle {
+getArea() double
}
class Rectangle {
+getArea() double
}
class GeometricObject {
+getArea()* double
}
GeometricObject <|-- Circle
GeometricObject <|-- Rectangle
In C++, we declare a method as abstract by using the virtual keyword followed by 0 in the method declaration. Something like: virtual void getArea() const = 0; Think of the = 0; as saying βthere is no implementation at this levelβ. Child classes will provide implementations instead of saying = 0;. In the GeometricObject class, this will look like:
module;
#include <format>
#include <string>
export module GeometricObject;
using namespace std;
export class GeometricObject {
protected:
string m_color;
public:
// Constructor
GeometricObject(string color);
// getArea is an abstract function
// we have no way to do it at the base class level
virtual double getArea() const = 0;
// getColor is a regular virtual function
// it does not necessarily need to be virtual, it is unlikely
// to be overridden in derived classes
virtual string getColor() const;
// toString is a regular virtual function
// we can do the job here, but derived classes will likely want to override
// it to provide more specific information
virtual string toString() const;
};
GeometricObject::GeometricObject(string color)
{
this->m_color = color;
}
// getArea is abstract... nothing to list!
// note we do not use virtual outside the class
string GeometricObject::getColor() const
{
return m_color;
}
string GeometricObject::toString() const
{
return format("A {} object.", m_color);
}
Note that getColor and toString are regular virtual functions. We can do those jobs in the base class, so there is no need to make the abstract.
Now, when we go to implement the derived classes, we will need to provide specific implementations for the getArea method, ensuring that each geometric shape calculates its area correctly. Here is what Rectangle will look like:
module;
#include <format>
#include <string>
import GeometricObject;
export module Rectangle;
using namespace std;
export class Rectangle : public GeometricObject {
private:
double m_width;
double m_height;
public:
Rectangle(double width, double height, string color);
// provide an implementation of the getArea function
// it is no longer abstract
virtual double getArea() const;
// override the getStr function
virtual string toString() const;
};
Rectangle::Rectangle(double width, double height, string color)
: GeometricObject(color) {
m_width = width;
m_height = height;
}
double Rectangle::getArea() const {
return m_width * m_height;
}
string Rectangle::toString() const {
return format("A {} rectangle with dimensions of {} by {}.",
getColor(),
m_width,
m_height);
}
Circle will look just like Rectangle, only it will do different math in getArea() and print a different message in toString(). So we will omit a full listing here and leave that for the end of the page (ListingΒ 19.9.6). Now we can use these classes:
module;
#include <format>
#include <string>
import GeometricObject;
export module Circle;
using namespace std;
export class Circle : public GeometricObject {
private:
double m_radius;
public:
Circle(double radius, string color);
// same idea as Rectangle, but we will do different math
double getArea() const;
virtual string toString() const;
};
Circle::Circle(double radius, string color)
: GeometricObject(color) {
m_radius = radius;
}
double Circle::getArea() const {
return std::numbers::pi * m_radius * m_radius;
}
string Circle::toString() const {
return format("A {} circle with a radius of {}.", m_color, m_radius);
}