Abstract classes and interfaces

An abstract class is a class with at least one pure virtual function: a function the base declares and refuses to define. The class cannot be instantiated, and every concrete derived class has to provide the missing functions.

That makes it an interface: a description of behaviour with no data and no implementation. The rest of the program talks to the interface, and the concrete classes are chosen where the objects are created. Lesson 035 supplied the dispatch; this lesson uses it to separate what a program does from how it does it.

A pure virtual function

The = 0 after the declaration makes a function pure virtual: the base has no body, and the class becomes abstract. A derived class that overrides every pure function becomes concrete and can be created.

The program tries to create the abstract base directly, and the compiler refuses. The message names the class as abstract and lists the pure function that has no implementation.

The refusal is the point of the feature: an object of the base type would have no area() to call, so the language does not allow one to exist.

An abstract class may still have ordinary members, data and constructors. The pure function only means that the type is incomplete, not that the class is empty, and a hierarchy can put the shared state in the abstract base and the behaviour in the leaves.

class shape { public: virtual ~shape() = default; virtual int area() const = 0; }; int main() { shape s; return s.area(); }

g++ -std=c++23 -Wall -Wextra pure.cpp -o pure

pure.cpp: In function 'int main()':
pure.cpp:11:11: error: cannot declare variable 's' to be of abstract type 'shape'
11 | shape s;
| ^
pure.cpp:1:7: note: because the following virtual functions are pure within 'shape':
1 | class shape
| ^~~~~
pure.cpp:6:17: note: 'virtual int shape::area() const'
6 | virtual int area() const = 0;
| ^~~~

Two classes behind one interface

The interface below has a virtual destructor, two pure virtual functions and no data. square and rectangle implement it, and the program holds each one behind a reference to the interface.

The calls to area() and name() reach the concrete class. The interface does not know either class, and neither class knows the other; they share only the declaration of what a shape can do.

A virtual destructor is part of the interface because callers may delete through it, as lesson 035 explained. An interface without one is a base class that cannot be owned polymorphically.

Notice that the interface has no data at all. That is what makes it reusable: the classes that implement it are free to store whatever they need, and a caller that only sees shape cannot depend on any of it.

#include <iostream> class shape { public: virtual ~shape() = default; virtual int area() const = 0; virtual const char* name() const = 0; }; class square : public shape { public: explicit square(int side) : side_(side) {} int area() const override { return side_ * side_; } const char* name() const override { return "square"; } private: int side_; }; class rectangle : public shape { public: rectangle(int width, int height) : width_(width), height_(height) {} int area() const override { return width_ * height_; } const char* name() const override { return "rectangle"; } private: int width_; int height_; }; int main() { square s{4}; rectangle r{2, 3}; const shape& first = s; const shape& second = r; std::cout << first.name() << ' ' << first.area() << '\n'; std::cout << second.name() << ' ' << second.area() << '\n'; }

g++ -std=c++23 -Wall -Wextra interface.cpp -o interface
./interface

square 16
rectangle 6

Injecting the interface

A class that stores a reference to the interface can work with any implementation, and the choice is made by whoever constructs it. That is dependency injection without a framework: the dependency is a constructor parameter.

The canvas below holds const shape& and prints its area. It has no idea whether the shape is a square or something written next year, and it does not own the shape, so the lifetime stays with main.

The same pattern with a pointer would allow the dependency to be replaced or to be absent; with a reference it is fixed for the life of the object. Both are ordinary C++, and neither needs a container.

The benefit shows up in tests: a test can pass an implementation that records calls or returns fixed numbers, and the canvas code does not change. The cost is one indirection per call, the same one lesson 035 measured.

#include <iostream> class shape { public: virtual ~shape() = default; virtual int area() const = 0; }; class square : public shape { public: explicit square(int side) : side_(side) {} int area() const override { return side_ * side_; } private: int side_; }; class canvas { public: explicit canvas(const shape& s) : shape_(s) {} void draw() const { std::cout << "area " << shape_.area() << '\n'; } private: const shape& shape_; }; int main() { square s{5}; canvas c{s}; c.draw(); }

g++ -std=c++23 -Wall -Wextra inject.cpp -o inject
./inject

area 25

A half-implemented class stays abstract

A derived class that implements one pure function and forgets another is still abstract. The compiler will not let it be created, and the message lists the function that is still pure.

The program's square overrides area() but not name(), so square s; fails. The note points at name() in the interface, which is the missing piece.

This is a useful property in a hierarchy of interfaces: a partial implementation can exist as a base for further classes, and the compiler keeps track of what is still missing at every level.

The rule also means that adding a pure function to an interface breaks every concrete class that implements it, which is why interfaces are kept small and stable.

class shape { public: virtual ~shape() = default; virtual int area() const = 0; virtual const char* name() const = 0; }; class square : public shape { public: int area() const override { return 4; } }; int main() { square s; return s.area(); }

g++ -std=c++23 -Wall -Wextra partial.cpp -o partial

partial.cpp: In function 'int main()':
partial.cpp:21:12: error: cannot declare variable 's' to be of abstract type 'square'
21 | square s;
| ^
partial.cpp:10:7: note: because the following virtual functions are pure within 'square':
10 | class square : public shape
| ^~~~~~
partial.cpp:7:25: note: 'virtual const char* shape::name() const'
7 | virtual const char* name() const = 0;
| ^~~~

Exercises

  1. Add a circle class to interface.cpp with its own area() and name(), and call both through a shape reference.
  2. Change canvas in inject.cpp to hold a pointer instead of a reference and add a set_shape() method.
  3. Implement name() in partial.cpp and check that the program compiles. Which function does the compiler ask for next?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is a pure virtual function?

    Answer

    A virtual function declared with = 0 that the base does not define

    The declaration reserves the slot, and every concrete derived class has to provide the body.

  2. Can an abstract class be instantiated?

    Answer

    No, the compiler rejects a variable of that type

    The object would have a function with no implementation, so the type cannot be complete.

  3. What must a concrete derived class do?

    Answer

    Implement every pure virtual function of the base

    Any missing override leaves the derived class abstract as well.

  4. What is an interface in this lesson?

    Answer

    An abstract class with the behaviour and no data

    The program talks to the interface, and the concrete classes are chosen by the caller.

  5. What does declaring shape s; produce?

    Answer

    cannot declare variable 's' to be of abstract type 'shape'

    The pure virtual function is named in the note, and the variable cannot be created.

  6. What does this program print? #include <iostream> struct shape { virtual ~shape() = default; virtual const char* name() const = 0; virtual int area() const = 0; }; struct square : shape { const char* name() const override { return "square"; } int area() const override { return 16; } }; struct rectangle : shape { const char* name() const override { return "rectangle"; } int area() const override { return 6; } }; int main() { square a; rectangle b; std::cout << a.name() << ' ' << a.area() << '\n' << b.name() << ' ' << b.area(); }

    Answer

    square 16 / rectangle 6

    Each derived class implements the two pure functions, and the two calls print their values.

  7. What does this program print? #include <iostream> struct shape { virtual ~shape() = default; virtual int area() const = 0; }; struct square : shape { int area() const override { return 25; } }; int report(const shape& s) { return s.area(); } int main() { square s; std::cout << "area " << report(s); }

    Answer

    area 25

    The function receives the interface and calls the area of the square it was given.

  8. What does the half-implemented class report?

    Answer

    cannot declare variable 's' to be of abstract type 'square'

    The missing name() function keeps square abstract, and the note names it.

  9. Why pass an interface reference instead of a concrete object?

    Answer

    The function works with any implementation the caller has

    The dependency is on the behaviour, not on the class that provides it.

  10. What happens if a derived class implements all but one pure function?

    Answer

    It is abstract too and cannot be created

    Abstractness is a property of the type, and it lasts until every pure function has a body.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev