Let us explore pure virtual functions and abstract classes in C++.
Pure Virtual Functions
In C++, a pure virtual function is a special kind of virtual function that serves as a placeholder within a base class. It is declared using the following syntax:
virtual void function_name(parameters) = 0;
Key characteristics of a pure virtual function:
- No implementation: The pure virtual function has no function body within the base class. The `= 0` notation signifies this.
- Mandatory override: Any derived class that inherits from the base class *must* provide a concrete implementation for the pure virtual function; otherwise, the derived class itself becomes abstract.
A class containing at least one pure virtual function is called an
abstract class. Here are their essential features:
- Cannot be instantiated: You cannot create objects directly from an abstract class. It primarily serves as a blueprint or interface for derived classes.
- Enforce inheritance: Abstract classes ensure that derived classes provide the necessary implementations for specific behaviors defined by the pure virtual functions.
Usefulness of Pure Virtual Functions and Abstract Classes
- Defining Interfaces: The primary purpose of this combination is to establish a contract or an interface. The abstract class defines the essential methods (pure virtual functions) that all its derived classes must implement. This promotes consistency and ensures that derived classes conform to a specific structure.
- Polymorphism: Abstract classes and pure virtual functions are the foundation of polymorphism in C++. Derived classes can provide their unique implementations for the inherited pure virtual functions. This allows you to treat objects of different derived classes in a uniform way through a pointer or reference to the abstract base class.
Example
class Shape { // Abstract class
public:
virtual void calculateArea() = 0; // Pure virtual function
};
class Rectangle : public Shape {
public:
void calculateArea() override {
// Implementation for calculating the area of a rectangle
}
};
class Circle : public Shape {
public:
void calculateArea() override {
// Implementation for calculating the area of a circle
}
};
A type hierarchy usually has its base class contain a number of virtual functions. Virtual functions provide for dynamic typing.
These virtual functions in the base class are often dummy functions. They have an empty body in the base class, but they will be given specific meanings in the derived classes. In C++, the
pure virtual function is introduced for this purpose. A pure virtual function is a virtual member function whose body is normally undefined. Notationally, a pure virtual function is declared inside the class as follows:
virtual function prototype = 0;
For example:
class Abstract_Base {
public:
//interface - largely virtual
Abstract_Base(); // default constructor
//copy constructor
Abstract_Base(const Abstract_Base&);
// destructor should be virtual
virtual ~Abstract_Base();
// pure virtual function for print
virtual void print() = 0;
...
}
The pure virtual function is used to defer the implementation decision of the function. In OOP terminology it is called a
deferred method.
A class that has at least one pure virtual function is an
abstract class. It is useful for the base class in a type hierarchy to be an abstract class. It would have the basic common properties of its derived classes, but cannot itself be used to declare objects.Instead, it is used to declare pointers that can access subtype objects derived from the abstract class.
- Abstract Classes and Pure Virtual Functions
A base class reference or pointer can be assigned a derived class object or address. Manipulation of such a reference or pointer can be polymorphic by using virtual functions, the properly overridden function defined in the derived class is called dynamically. Usually, such a base class is abstract. This identifies the class as an important type to be used polymorphically. It guarantees that the compiler will insist on overridden member function definitions where concrete behavior for derived types is needed.
- Virtual Functions: To use heterogeneous lists effectively, we need functions that can be applied to any object on the list without regard to its class. Such functions will generally have different definitions for different classes. We want the appropriate definition to be used whenever a function is applied to an object, even though the class of the object was not known at compile. In C++ these capabilities are provided by virtual functions.
Should an overridden virtual function throw an exception?
Only if the base class says it might. It is appropriate for an override to throw an exception if and only if the specification of the member function in the base class says it might (there is a special case for legacy code that considers the absence of an exception specification in the base class to allow the derived class to throw whatever it wants, but this is for historical reasons only). Without such a specification in the base class, throwing an exception in an override is similar to false advertising. For example, consider a user-car salesman selling a kind-of car that blows up (that is, throws an exception) when you turn on the ignition switch. Code should do what the specification says it will do, or other code that relies on those promises may break.
Also, consider the Ostrich/Bird dilemma. Suppose Bird::fly() promises never to throw an exception, as follows.
#include <iostream>
using namespace std;
class Bird {
public:
Bird() throw();
virtual ~Bird() throw();
int altitude() const throw();
virtual void fly() throw();
// PROMISE: altitude() will return a number > 0; never throws an exception.
protected:
int altitude_;
};
Bird::Bird() throw(): altitude_(0){ }
Bird::~Bird() throw() { }
int Bird::altitude() const throw(){
return altitude_;
}
void Bird::fly() throw(){
altitude_ = 100;
}
Based on this promise, it is legitimate and appropriate to assume that the fly() member function will not throw an exception. For example, the following sample code is decorated with a throw(), meaning that this function promises not to throw an exception.
// Legitimate reliance on what Bird::fly() says
void sample(Bird& bird) throw() {
bird.fly();
}
But suppose Ostrich::fly() is defined to throw an exception, as follows.
class CannotFly { };
class Ostrich : public Bird {
public:
virtual void fly() throw(CannotFly);
// PROMISE: throws an exception despite what Bird says
};
void Ostrich::fly() throw(CannotFly){throw CannotFly(); }
Now suppose someone legitimately passes an Ostrich into the sample() code:
int main(){
Ostrich bird;
sample(bird); // Legitimate conversion from Ostrich to Bird
}
Member function throws an Exception
Unfortunately the program will crash in the sample() function, since the fly() member function ends up throwing an exception.One cannot blame main() for passing an Ostrich into the sample() function; after all, Ostrich inherited from Bird and therefore Ostrich is supposed to be substitutable for Bird. One cannot blame sample() for believing the promise made by Bird::fly(); indeed programmers are supposed to rely on the specification rather than the implementation. So the blame rests with the author of class Ostrich, who claimed that Ostrich was substitutable for Bird even though it didn not behave like a Bird. The lesson is that improper inheritance cannot be fixed by throwing an exception if the base class prohibits the throwing of an exception. This is because the root of improper inheritance is behavior that violates a contract, and throwing an exception is part of a function's behavior. Specifically, the behavior of an overridden virtual function that throws an exception conflicts with a base class contract that prohibits the throwing of an exception.
Abstract Classes - Quiz
Click the Quiz link below to take a brief multiple-choice quiz on abstract classes and virtual functions.
Abstract Classes - Quiz