Defining Abstract Interfaces
Define interfaces with pure virtual functions that derived classes must implement.
What Are Pure Virtual Functions, Abstract Base Classes, and Interface Classes?
A pure virtual function is a virtual function declared with = 0 where a body would normally go, which hands the job of writing that body to the derived classes. A class holding even one un-overridden pure virtual function is an abstract base class, and the compiler will not let you create an object of it. An interface class is the extreme form of the same idea: no member variables at all, every member function pure virtual, so the class is nothing but a list of operations that implementers are obliged to supply.
Those three names describe one mechanism seen at three levels of strictness. This lesson works up through them, starting with the problem they exist to solve.
A base class with nothing honest to return
A base class collects what its derived classes have in common. Occasionally that includes an operation every derived class performs while the base class itself has no idea what the correct answer is.
Take a program that produces printed map sheets. Every projection flattens a curved planet onto flat paper, and every one of them has to distort something to do it. Mercator keeps local angles correct and inflates area near the poles. Mollweide keeps areas proportional and bends shapes. An Albers conic keeps areas proportional across a band of latitudes. Ask "what does a map projection preserve?" in the abstract and there is no answer, because the question only makes sense once you have picked one.
If the base class supplies a definition anyway, it has to invent something. The tempting move is to make the invented answer a plausible one, and that is exactly what makes the bug hard to spot.
This program compiles cleanly and prints a falsehood:
#include <iostream>
#include <string>
#include <string_view>
class MapProjection
{
protected:
std::string m_sheetCode{};
public:
explicit MapProjection(std::string_view sheetCode)
: m_sheetCode{ sheetCode }
{
}
const std::string& sheetCode() const { return m_sheetCode; }
virtual std::string_view preserves() const { return "local angles"; }
virtual ~MapProjection() = default;
};
class AlbersEqualArea : public MapProjection
{
public:
explicit AlbersEqualArea(std::string_view sheetCode)
: MapProjection{ sheetCode }
{
}
// nobody wrote the override
};
int main()
{
AlbersEqualArea censusSheet{ "AE-057" };
std::cout << censusSheet.sheetCode() << " preserves "
<< censusSheet.preserves() << '\n';
return 0;
}
AE-057 preserves local angles
An equal-area projection preserves area, not angles. AlbersEqualArea never overrode preserves(), so the call resolved to the base class version, and the base class version was a guess. Nothing in the build objected, because as far as the language is concerned a virtual function with a body is a complete, usable function.
The fix is not a better guess. The fix is to make the base class say that it has no answer, in a form the compiler enforces.
Writing = 0 instead of a body
Replace the body of a virtual function with = 0 and it becomes a pure virtual function, sometimes called an abstract function. The = 0 is a piece of syntax called a pure specifier; it does not assign anything, and the function does not return zero.
A class can mix all three kinds of member function freely:
#include <string_view>
class MapProjection
{
public:
std::string_view unitLabel() const { return "metre"; } // one body, no dispatch
virtual std::string_view familyName() const { return "cylindrical"; } // dispatched, and replaceable
virtual std::string_view preserves() const = 0; // dispatched, and every implementer owes one
virtual ~MapProjection() = default;
};
The three rows differ only in how much freedom the derived class has, and the pure virtual row is the only one that removes freedom rather than adding it.
| Declaration | Base class supplies a body | Derived class may override | Derived class must override |
|---|---|---|---|
std::string_view unitLabel() const { ... } |
Yes | No | No |
virtual std::string_view familyName() const { ... } |
Yes | Yes | No |
virtual std::string_view preserves() const = 0; |
Not required | Yes | Yes |
The pure specifier is only meaningful on a virtual function, since the whole point is to be filled in through dynamic dispatch. Putting it on a non-virtual member is an error.
This does not compile:
#include <string_view>
class MapProjection
{
public:
int sheetCount() const = 0; // nothing dispatches here, so there is nothing to fill in
virtual ~MapProjection() = default;
};
s.cpp:6:9: error: initializer specified for non-virtual method 'int MapProjection::sheetCount() const'
6 | int sheetCount() const = 0; // nothing dispatches here, so there is nothing to fill in
| ^~~~~~~~~~
Some texts call a class that merely gets inherited from an "abstract base class". In C++ the term is precise: a class is abstract if and only if it has at least one pure virtual function that has not been overridden. Nothing else, including a protected constructor or a name ending in
Base, makes a class abstract.
Why the object cannot be built
Once a class holds a pure virtual function, the compiler stops creating objects of that type. It is not being fussy. If MapProjection could be built as an object, then preserves() could be called on it, and there is no code for that call to reach.
The refusal is a compile error, not a runtime failure:
#include <iostream>
#include <string>
#include <string_view>
class MapProjection
{
protected:
std::string m_sheetCode{};
public:
explicit MapProjection(std::string_view sheetCode)
: m_sheetCode{ sheetCode }
{
}
const std::string& sheetCode() const { return m_sheetCode; }
virtual std::string_view familyName() const = 0;
virtual std::string_view preserves() const = 0;
virtual ~MapProjection() = default;
};
int main()
{
MapProjection blank{ "NA-118" };
std::cout << blank.preserves() << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:26:19: error: cannot declare variable 'blank' to be of abstract type 'MapProjection'
26 | MapProjection blank{ "NA-118" };
| ^~~~~
s.cpp:5:7: note: because the following virtual functions are pure within 'MapProjection':
5 | class MapProjection
| ^~~~~~~~~~~~~
• 'virtual std::string_view MapProjection::familyName() const'
s.cpp:18:30:
18 | virtual std::string_view familyName() const = 0;
| ^~~~~~~~~~
• 'virtual std::string_view MapProjection::preserves() const'
s.cpp:19:30:
19 | virtual std::string_view preserves() const = 0;
| ^~~~~~~~~
Notice what the ban does and does not cover. You cannot create a MapProjection object, cannot store one by value in a container, and cannot return one by value. You absolutely can declare a MapProjection* or a MapProjection&, and doing so is the entire point: those are the handles through which polymorphic code talks to whichever derived object actually exists.
Notice too that the class still has a constructor and a data member, both of which run and exist. An abstract base class is a normal class in every way except that its construction has to happen as part of building a derived object.
Abstractness is inherited until somebody clears it
The obligation to override does not stop at the first derived class. A derived class inherits every pure virtual function it did not override, and inherits abstractness along with them.
Declaring such a class is perfectly legal; the compiler says nothing until you try to make one:
#include <iostream>
#include <string>
#include <string_view>
class MapProjection
{
protected:
std::string m_sheetCode{};
public:
explicit MapProjection(std::string_view sheetCode)
: m_sheetCode{ sheetCode }
{
}
const std::string& sheetCode() const { return m_sheetCode; }
virtual std::string_view familyName() const = 0;
virtual std::string_view preserves() const = 0;
virtual ~MapProjection() = default;
};
class Gnomonic : public MapProjection
{
public:
explicit Gnomonic(std::string_view sheetCode)
: MapProjection{ sheetCode }
{
}
std::string_view familyName() const override { return "azimuthal"; }
// nobody overrode preserves()
};
int main()
{
Gnomonic routePlanner{ "GR-021" };
std::cout << routePlanner.familyName() << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:39:14: error: cannot declare variable 'routePlanner' to be of abstract type 'Gnomonic'
39 | Gnomonic routePlanner{ "GR-021" };
| ^~~~~~~~~~~~
s.cpp:24:7: note: because the following virtual functions are pure within 'Gnomonic':
24 | class Gnomonic : public MapProjection
| ^~~~~~~~
• 'virtual std::string_view MapProjection::preserves() const'
s.cpp:19:30:
19 | virtual std::string_view preserves() const = 0;
| ^~~~~~~~~
This is the mechanism that would have caught the opening bug. AlbersEqualArea would have been abstract, main() would have refused to build it, and the compiler would have named the missing function.
An inheritance chain can therefore stay abstract through as many levels as it likes. Each class is measured by the same rule: count the pure virtual functions still outstanding.
| The class | Pure virtual functions outstanding | Objects of it |
|---|---|---|
| Declares a pure virtual function | At least one | Cannot be created |
| Inherits one and overrides none | Still outstanding | Cannot be created |
| Inherits two and overrides one | One remaining | Cannot be created |
| Overrides every inherited pure virtual | None | Can be created |
Once a class clears the last one, it is a concrete class and behaves like any other:
#include <iostream>
#include <string>
#include <string_view>
class MapProjection
{
protected:
std::string m_sheetCode{};
public:
explicit MapProjection(std::string_view sheetCode)
: m_sheetCode{ sheetCode }
{
}
const std::string& sheetCode() const { return m_sheetCode; }
virtual std::string_view familyName() const = 0;
virtual std::string_view preserves() const = 0;
virtual ~MapProjection() = default;
};
class Mercator : public MapProjection
{
public:
explicit Mercator(std::string_view sheetCode)
: MapProjection{ sheetCode }
{
}
std::string_view familyName() const override { return "cylindrical"; }
std::string_view preserves() const override { return "local angles"; }
};
class Mollweide : public MapProjection
{
public:
explicit Mollweide(std::string_view sheetCode)
: MapProjection{ sheetCode }
{
}
std::string_view familyName() const override { return "pseudocylindrical"; }
std::string_view preserves() const override { return "relative area"; }
};
void printSheet(const MapProjection& plate)
{
std::cout << plate.sheetCode() << ": " << plate.familyName()
<< ", preserves " << plate.preserves() << '\n';
}
int main()
{
Mercator harbourChart{ "NA-118" };
Mollweide worldPlate{ "WA-004" };
printSheet(harbourChart);
printSheet(worldPlate);
return 0;
}
NA-118: cylindrical, preserves local angles
WA-004: pseudocylindrical, preserves relative area
printSheet() takes a const MapProjection&, a reference to a type no object can ever have. Both calls bind that reference to a derived object, and both pure virtual calls dispatch through the vtable exactly like ordinary virtual calls. Pure virtual functions are not a separate calling mechanism; they are ordinary virtual functions with the base definition withheld.
When a base class exists to define a common set of operations rather than to be used on its own, make the operations it cannot implement pure virtual. Turning "I forgot to override this" from a wrong answer at runtime into a named error at compile time is the whole return on the syntax.
A pure virtual function may still have a body
Here is the part that surprises almost everybody. The pure specifier and a function body are not mutually exclusive. = 0 says "derived classes must override this". It does not say "this function has no code".
The one restriction is placement: the definition has to be written outside the class, because the syntax has no room for both = 0 and a brace-enclosed body in the declaration.
#include <iostream>
#include <string>
#include <string_view>
class MapProjection
{
protected:
std::string m_sheetCode{};
public:
explicit MapProjection(std::string_view sheetCode)
: m_sheetCode{ sheetCode }
{
}
const std::string& sheetCode() const { return m_sheetCode; }
virtual int graticuleSpacing() const = 0; // the specifier stays
virtual ~MapProjection() = default;
};
int MapProjection::graticuleSpacing() const // and the body lives out here
{
return 15;
}
class Cassini : public MapProjection
{
public:
explicit Cassini(std::string_view sheetCode)
: MapProjection{ sheetCode }
{
}
int graticuleSpacing() const override
{
return MapProjection::graticuleSpacing(); // the house default is fine here
}
};
class Gnomonic : public MapProjection
{
public:
explicit Gnomonic(std::string_view sheetCode)
: MapProjection{ sheetCode }
{
}
int graticuleSpacing() const override
{
return 5; // a route planner needs a denser grid
}
};
void printSpacing(const MapProjection& plate)
{
std::cout << plate.sheetCode() << " rules lines every "
<< plate.graticuleSpacing() << " degrees\n";
}
int main()
{
Cassini surveySheet{ "TR-402" };
Gnomonic routePlanner{ "GR-021" };
printSpacing(surveySheet);
printSpacing(routePlanner);
return 0;
}
TR-402 rules lines every 15 degrees
GR-021 rules lines every 5 degrees
MapProjection is still abstract, and Cassini still had to write an override. The difference is that its override was one line, because a default was available to delegate to. That is the entire pattern: a shared default that nobody gets by accident, only by asking for it with a qualified call. Compare it to the opening program, where AlbersEqualArea received a default it never asked for and never noticed.
Trying to put the body inline is a syntax error, and the diagnostic names the rule:
class MapProjection
{
public:
virtual int graticuleSpacing() const = 0 { return 15; } // = 0 and a body, in the class
virtual ~MapProjection() = default;
};
s.cpp:4:42: error: pure-specifier on function-definition
4 | virtual int graticuleSpacing() const = 0 { return 15; } // = 0 and a body, in the class
| ^
Bodies on pure virtual functions are uncommon in ordinary code, and a reader who has not met the trick will assume the class is broken. If you use it, say in a comment what the base version is there for. The one place it is not optional is the destructor, covered next.
Destructors can be pure virtual too
A destructor can carry the pure specifier, and it is the one case where the body is mandatory rather than optional. Destruction always walks the whole chain, so ~MapProjection() will run when a derived object is destroyed whether or not anybody wrote it. Declaring it pure without defining it leaves a call to a function that does not exist.
#include <iostream>
#include <string>
#include <string_view>
class MapProjection
{
protected:
std::string m_sheetCode{};
public:
explicit MapProjection(std::string_view sheetCode)
: m_sheetCode{ sheetCode }
{
}
virtual std::string_view familyName() const { return "cylindrical"; }
virtual ~MapProjection() = 0; // the specifier, on the destructor
};
MapProjection::~MapProjection() // whose body the linker will insist on
{
std::cout << "filed plate " << m_sheetCode << '\n';
}
class Cassini : public MapProjection
{
public:
explicit Cassini(std::string_view sheetCode)
: MapProjection{ sheetCode }
{
}
std::string_view familyName() const override { return "transverse cylindrical"; }
~Cassini() override { std::cout << "cleared the survey grid\n"; }
};
int main()
{
MapProjection* plate{ new Cassini{ "TR-402" } };
std::cout << plate->familyName() << '\n';
delete plate;
return 0;
}
transverse cylindrical
cleared the survey grid
filed plate TR-402
Note that familyName() here is an ordinary virtual function with a body, and Cassini overrides nothing else. The destructor alone is what makes MapProjection abstract. That is the reason to reach for this: a class with no operation worth marking pure, which you still want to stop anyone from instantiating.
Omit the out-of-class
MapProjection::~MapProjection() definition and the code still compiles. It fails at link time instead, with an undefined reference to MapProjection::~MapProjection() from every derived destructor. A pure virtual destructor is a declaration that you owe the linker a body.
An abstract base class exists to be used through base pointers, so sooner or later one of those pointers gets deleted. Its destructor therefore needs to be public and virtual.
virtual ~MapProjection() = default; covers that; reach for = 0 on the destructor only when you need abstractness and have no other operation worth marking.
Interface classes: a contract with no state
Push the idea to its limit. Take a class, remove every member variable, and make every member function pure virtual apart from the destructor. What is left carries no data and no behaviour, only a list of operations that any implementer must provide. That is an interface class.
By convention these are named with a leading I, which tells a reader at the call site that they are looking at a contract rather than a concrete type.
Suppose the map sheets need overlays drawn on top of them: a graticule of latitude and longitude lines, contour shading, hill shading, a UTM grid. Each one knows its own name and its own line weight, and they have nothing else in common, certainly no shared data.
#include <iostream>
#include <string_view>
class IGridOverlay
{
public:
virtual std::string_view overlayTag() const = 0;
virtual int lineWeight() const = 0;
virtual ~IGridOverlay() = default;
};
class GraticuleLines : public IGridOverlay
{
public:
std::string_view overlayTag() const override { return "graticule"; }
int lineWeight() const override { return 1; }
};
class ContourShading : public IGridOverlay
{
public:
std::string_view overlayTag() const override { return "contours"; }
int lineWeight() const override { return 3; }
};
void describeSheet(std::string_view sheetCode, const IGridOverlay& overlay)
{
std::cout << sheetCode << " draws " << overlay.overlayTag()
<< " at weight " << overlay.lineWeight() << '\n';
}
int main()
{
GraticuleLines thinGrid{};
ContourShading relief{};
describeSheet("NA-118", thinGrid);
describeSheet("WA-004", relief);
return 0;
}
NA-118 draws graticule at weight 1
WA-004 draws contours at weight 3
What the interface bought
The value shows up in the signature of describeSheet(), not in the class definitions. Written against a concrete type, that function compiles and works, and locks its callers to one overlay forever:
#include <iostream>
#include <string_view>
class GraticuleLines
{
public:
std::string_view overlayTag() const { return "graticule"; }
int lineWeight() const { return 1; }
};
void describeSheet(std::string_view sheetCode, const GraticuleLines& overlay)
{
std::cout << sheetCode << " draws " << overlay.overlayTag()
<< " at weight " << overlay.lineWeight() << '\n';
}
A caller who wants contour shading has to get describeSheet() edited. A caller who wants hill shading has to get it edited again. Every new overlay is a change to code that has no interest in overlays.
Take a const IGridOverlay& instead and the function is finished. Contour shading works. Hill shading works. An overlay somebody writes next year, in a file this one has never heard of, works, provided it implements the two functions. The function depends on the contract, and the contract does not change when implementations are added.
That is what an interface class is for: it lets code state which operations it needs without stating which type will supply them.
An interface class must declare a virtual destructor. It has no data of its own, which makes it easy to conclude that destruction is not its problem, but the implementers behind the interface may hold anything at all. Delete through an
IGridOverlay* without a virtual destructor and their destructors never run.
Languages such as Java and C# promote this pattern into a keyword and let a class implement as many interfaces as it likes, while restricting normal inheritance to one base. C++ has no interface keyword, so the pattern is expressed with the tools in this lesson. The reasoning behind those languages' rule is worth borrowing, though: because an interface has no data and no function bodies, inheriting several of them avoids most of the problems that multiple inheritance usually brings.
What the virtual table holds for a pure virtual
Abstract classes still get a virtual table. That may look wasteful for a type that can never be instantiated, but it is needed: the base class subobject is real while a derived object is being built and torn down, and a constructor or destructor of an abstract class can call a virtual function. During those windows the derived part is not available, so the call has to resolve to the abstract class's own version, which means the abstract class needs a vtable of its own.
The slot for a pure virtual function has to hold something. Implementations typically store either a null pointer or the address of a shared diagnostic function, often named __purecall, which reports the problem and terminates. That slot is unreachable through any correctly written program, since no object of the abstract type exists. It is reachable by mistake, and calling a pure virtual function from inside a base class constructor or destructor is the classic route there.
Summary
Pure virtual functions: A virtual function declared with = 0 in place of a body is pure virtual, for example virtual std::string_view preserves() const = 0;. The pure specifier is a promise that derived classes will supply the implementation, and it is only valid on virtual functions; putting it on a non-virtual member is an error.
Abstract base classes: A class with at least one un-overridden pure virtual function is abstract, and the compiler rejects any attempt to create an object of it. Pointers and references to the type remain legal and are how polymorphic code uses the hierarchy.
Abstractness is inherited: A derived class that does not override every pure virtual function it inherits is abstract as well. Declaring such a class is fine; the error appears at the point where something tries to instantiate it, and the diagnostic names the functions still outstanding.
Bodies on pure virtual functions: A pure virtual function may be given a definition, which must be written outside the class. The class stays abstract and derived classes must still override, but an override can delegate to the base version with a qualified call, giving a shared default that has to be requested rather than inherited by accident.
Pure virtual destructors: A destructor can be made pure to force abstractness on a class with nothing else to mark, but it must be given a definition, because destruction runs the whole chain regardless. Give every abstract base class a public virtual destructor, whether or not the destructor is the thing making it abstract.
Interface classes: An interface class has no member variables and every function pure virtual, so it defines operations without defining data or behaviour. They are conventionally named with a leading I.
Why interfaces pay off: Code written against an interface works with every current implementation and every future one, because the dependency is on the list of operations rather than on a concrete type. That decoupling, not any performance gain, is the reason to use one.
Virtual tables: Abstract classes still have virtual tables, since a constructor or destructor of an abstract class can make a virtual call that must resolve within that class. The vtable slot for a pure virtual function holds a null pointer or the address of a diagnostic function, sometimes named __purecall.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Defining Abstract Interfaces - Quiz
Test your understanding of the lesson.
Practice Exercises
Abstract Base Classes and Interfaces
Create abstract base classes using pure virtual functions. Implement interface classes that define contracts for derived classes.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!