Abstract classes and multiple inheritance
Inheritance, operators, and templates13 min readLesson 3 of 5
3.1 A class that cannot be instantiated
A pure virtual function is declared with = 0 and normally has no body in the class. A class with at least one pure virtual function is abstract. The compiler refuses to create an object of an abstract type, because the object would lack an implementation for that function.
A derived class becomes concrete when it overrides every pure virtual function it inherits. If it misses one, it stays abstract. This rule lets a base class define a complete interface while leaving each implementation to a derived class.
#include <iostream>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Rect : public Shape {
public:
Rect(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
private:
double w_, h_;
};
void show(const Shape& s) { std::cout << "Area: " << s.area() << "\n"; }
int main() {
Rect rect(3.0, 4.0);
show(rect);
return 0;
}Output
Area: 12
3.1.1 Programming to an interface
An abstract class defines what callers can do and leaves the details to derived classes. Code that takes a const Shape& works with every shape, including ones written later. The base class does not need to know the full list of derived types.
A pure virtual function leaves the implementation to derived classes, so every concrete derived class supplies one. Add a virtual destructor to an abstract base so deletion through a base pointer reaches the derived destructor. The destructor can be = default; or empty. A pure virtual destructor still needs a definition, because derived destructors call it.
3.1.2 Multiple inheritance and ambiguity
A class can inherit from more than one base: class File : public Reader, public Writer. Construction follows the base list from left to right, and destruction runs in reverse. Each base contributes its own members and its own interface.
If two bases declare a member with the same name, an unqualified use in the derived class is ambiguous. The compiler lists both candidates and stops. Qualify the call with the base name, as in Reader::open(), or add a derived function that chooses between them. Ambiguity is a compile-time error, so it surfaces before the program runs.
#include <iostream>
class Reader {
public:
void open() const { std::cout << "read mode\n"; }
};
class Writer {
public:
void open() const { std::cout << "write mode\n"; }
};
class File : public Reader, public Writer {
public:
void open_both() const { Reader::open(); Writer::open(); }
};
int main() {
File file;
file.open_both();
return 0;
}Output
read mode write mode
3.1.3 The diamond problem
Multiple inheritance can produce a diamond. Book and Ebook both inherit from Item, and Bundle inherits from both. A plain Bundle then contains two Item subobjects, so bundle.id is ambiguous and the two ids can disagree.
Virtual inheritance makes Item appear once in the most derived object. Book : virtual public Item and Ebook : virtual public Item share the same base. The most derived class then initializes that shared base. Virtual inheritance adds complexity to construction and layout, so reserve it for hierarchies where shared state is genuinely needed.
#include <iostream>
struct Item { int id = 0; };
struct Book : virtual Item {};
struct Ebook : virtual Item {};
struct Bundle : Book, Ebook {};
int main() {
Bundle bundle;
bundle.id = 42;
std::cout << "Id: " << bundle.id << "\n";
return 0;
}Output
Id: 42
3.1.4 Interfaces and mixins
Multiple inheritance works well when the bases are small interfaces with no data. A Document can implement Named and Sized without sharing state between them, and code can pass the same object through either interface. Each base adds a role without adding a second copy of the object.
Keep interface bases free of data members and give each one a virtual destructor. When the bases carry state, the diamond risk and construction order become harder to reason about. Composition often handles a combination of behaviors with fewer surprises.
3.1.5 Constructors and destructors in a hierarchy
A base class constructor runs before the derived constructor, so a base that needs data must receive it through the derived initializer list. An abstract base can still have a constructor, and derived classes call it like any other base constructor.
Declare a virtual destructor in any base class that callers may delete through a base pointer. The base destructor body runs after the derived destructor body, so each level releases its own resources in the right order. The most derived constructor supplies the arguments, and each intermediate class passes them along to its own base.
#include <iostream>
struct Reader {
virtual ~Reader() = default;
virtual void read() const = 0;
};
struct Writer {
virtual ~Writer() = default;
virtual void write() const = 0;
};
class File : public Reader, public Writer {
public:
void read() const override { std::cout << "reading\n"; }
void write() const override { std::cout << "writing\n"; }
};
int main() {
File file;
const Reader& reader = file;
reader.read();
file.write();
}Output
reading writing
3.1.6 Common mistakes
- Trying to create an object of an abstract class.
- Forgetting to override one pure virtual function, which leaves the derived class abstract.
- Omitting the virtual destructor and deleting through a base pointer.
- Assuming multiple inheritance merges duplicate base members without virtual inheritance.
- Calling an ambiguous base member without qualification.
- Using multiple inheritance to combine implementations when composition would be clearer.
3.1.7 Practice
- Add a
Circleclass that implementsShapeand print its area throughshow. - Remove
overridefrom one pure virtual implementation and confirm the class becomes abstract. - Build two bases with an
openfunction and qualify both calls in the derived class. - Change the diamond example to plain inheritance and try to access
id. - Write two interface classes with no data and make one class implement both.
