C++ Part I · Module 17 · Runtime Polymorphism
Pure virtual Functions and Abstract Classes
Every base class in this module so far has had working implementations, and that choice has a consequence worth naming: a base with a working function supplies a default, not a contract. Two things follow, and both are on this module's list of classic failures. A derived class that forgets to implement something inherits the base's version silently, so a missing implementation is not an error the build reports but a wrong answer the program produces. And the base itself can be instantiated, which means an object can exist that is meant to represent any model and in fact represents none of them. This chapter is the construct that fixes both by writing two characters, and the interesting part is not the syntax but the inventory of things it refuses. The base cannot be made into an object. A derived class that implements only some of the required functions is itself not instantiable, and says so in the same words. A function cannot take the base by value — though exactly where that error appears is more subtle than usually stated, and this chapter measures it in four positions, two of which compile. And the base cannot go in a container of values. Those last two refusals are worth pausing on, because they are the same refusal, and between them they make the bug the next-but-one chapter is about a compile error rather than a silent loss of behaviour.
Intermediate18 min readC++Part IFundamentalsPolymorphism
Part I · Module 17 · Chapter 17.4 · Pure virtual Functions and Abstract Classes
1. A Default Is Not a Contract
Chapter 17.2's VBase had a working run():
struct VBase { virtual ~VBase() = default; virtual void run() const { /* prints something */ } };A derived class that implements run replaces it. A derived class that forgets inherits it, and the program runs with the base's behaviour — no error, no warning, and the symptom appears wherever that behaviour is wrong.
So a base with working functions makes a statement of the form here is what happens if you do not say otherwise. What is usually wanted is you must say what happens, which is a different statement and needs different syntax.
2. Two Characters
virtual void step() = 0;The = 0 makes step a pure virtual function: declared, dispatchable, and with no implementation in this class. A class with at least one pure virtual function is an abstract class.
(The = 0 is syntax, not an assignment and not a null pointer. It is historical, and it means "there is no body here".)
struct Model {
virtual ~Model() = default;
virtual void step() = 0;
virtual const char* name() const = 0;
void runFor(int n) { for (int i = 0; i < n; ++i) step(); } // non-virtual, uses the pure ones
};
struct ReferenceModel : Model {
int ticks = 0;
void step() override { ++ticks; }
const char* name() const override { return "ReferenceModel"; }
};3. What the Compiler Knows
A. traits:
is_abstract<Model> = 1
is_abstract<ReferenceModel> = 0
is_default_constructible<Model> = 0Abstractness is a property the type system carries, not a convention. ReferenceModel is concrete because it implements everything, and Model is not default-constructible — not because it lacks a constructor, but because it cannot be an object at all.
4. Refusal One: You Cannot Make One
Model m;error: variable type 'Model' is an abstract classThat is the first half of the frozen description of this chapter — an interface that cannot be instantiated. The interface is no longer a type you might accidentally create; it is a shape other types must take.
5. Refusal Two: You Cannot Half-Implement
struct Partial : Model { }; // implements neither step() nor name()
Partial p;error: variable type 'Partial' is an abstract classThe same message, about the derived class. Partial inherited two pure virtual functions and overrode neither, so it is abstract too — and the error arrives at the first attempt to create one.
This is the second half: and must be implemented. Compare it with §1: a missing implementation has gone from a wrong answer at run time to a compile error at the point of use.
One consequence worth noting: a hierarchy may have abstract classes in the middle. A BusTransaction : Transaction that implements some of Transaction's interface and leaves the rest pure is a legitimate design — it is an abstract class that has narrowed the contract without completing it.
6. Refusal Three: Not by Value — and Where That Error Actually Appears
This is usually stated as "you cannot pass an abstract class by value", which is close enough to be useful and wrong in detail. Measured in four positions:
DECL_ONLY `void f(Model m);` -> COMPILES
DEFINE `void f(Model m) { ... }` -> error: parameter type 'Model' is an abstract class
CALL `f(r);` -> error: allocating an object of abstract class type 'Model'
RETURN `Model h();` -> COMPILESA bare declaration mentioning an abstract type by value is legal. The language only objects when something would have to create the object — defining the function, which needs a parameter, or calling it, which needs an argument.
The practical effect is the same, because a function you can neither define nor call is useless. But the precise rule is worth holding: the error is about constructing an object, not about naming the type. That is why §4, §5, §6 and §7 all produce one of two messages, both of which mention a variable or an allocation.
7. Refusal Four: Not in a Container of Values
std::vector<Model> v;
v.resize(1);error: allocating an object of abstract class type 'Model'Note that declaring the vector is fine — it is the same §6 rule. std::vector<Model> v; alone compiles, because nothing has been created yet. The error arrives at the first operation that would construct an element.
Chapter 14.1 §9 measured the alternative that works: a container of owning pointers. Here that is std::vector<std::unique_ptr<Model>>, which stores 8-byte pointers and constructs no Model objects at all.
8. What Still Works, Which Is Everything You Need
The refusals look restrictive until you notice that nothing useful was lost:
B. a reference to the abstract type works fine:
ReferenceModel ran, ticks=3
C. owning one through a base pointer:
ReferenceModel owned via unique_ptr<Model>
D. sizeof(Model)=8 sizeof(ReferenceModel)=16References and pointers to an abstract class are completely ordinary. Model& m = r; binds, m.runFor(3) dispatches into ReferenceModel::step three times, and std::unique_ptr<Model> owns a derived object through a base pointer exactly as Chapter 14.2 described.
And sizeof(Model) is 8 — an abstract class still has a size, because it still has a table pointer. It is a type you cannot instantiate, not a type that does not exist.
One structural thing to notice in §2's listing. runFor is a non-virtual public function that calls the pure virtual step. That inverts the usual arrangement: the public interface is fixed and the customisation points are the virtual functions behind it. Chapter 17.9 is where that shape is argued for properly; it is worth seeing here because an abstract class is what makes it possible.
9. The Refusal That Prevents the Next-But-One Chapter's Bug
§6 and §7 are the same rule, and together they have a consequence worth stating on its own:
ReferenceModel r;
Model m = r; // the copy that discards the derived parterror: variable type 'Model' is an abstract classSlicing is a compile error. Chapter 17.6 is the chapter on what happens when a derived object is copied into a base object — the derived part is silently discarded and subsequent virtual calls reach the base's implementations. Measured there with a concrete base, it is completely silent.
With an abstract base it cannot be written. There is no Model object for the derived part to be discarded into.
Making a polymorphic base abstract eliminates object slicing by construction. It is the strongest argument for doing it, and it costs two characters.
10. An Abstract Class Still Needs a Virtual Destructor
= 0 does nothing about destruction. An abstract base will be held and deleted through base pointers — that is what it is for — so it needs:
virtual ~Model() = default;Chapter 17.5 measures what is lost without one. The two constructs are independent and both are required: = 0 says the behaviour must be supplied; virtual ~T() says the destruction must be dispatched.
A related option, and one that exposes a fact about = 0 worth knowing: a pure virtual destructor (virtual ~Model() = 0;) is legal, and it must still be defined. Measured — omit the definition and it is not a compile error but a link error:
Undefined symbols for architecture arm64:
clang++: error: linker command failed with exit code 1That is because a derived destructor always calls its base's, so the body is genuinely needed. And it generalises:
struct Model {
virtual ~Model() = 0;
virtual void step() = 0;
};
Model::~Model() { /* ... */ } // required
void Model::step() { /* ... */ } // ALLOWED -- a pure virtual may have a bodyB. calling through the interface:
Real::step -> then the base's: Model::step (a pure virtual CAN have a body)
C. destruction:
~Real
~Model (a pure virtual dtor still needs a body)A pure virtual function may have an implementation, reachable only by explicit qualification — Model::step() — which is Chapter 17.2 §7's suppressed-dispatch form. And the class is still abstract with both bodies present:
Model m; -> variable type 'Model' is an abstract classSo = 0 does not mean no implementation. It means no implementation is inherited — derived classes must supply their own, and may optionally call the base's as shared scaffolding. That is occasionally useful for a common prologue, and it is the reason the two facts coexist: the destructor case is not an exception to = 0, it is the normal rule plus the fact that destructors are always chained.
A pure virtual destructor is mainly a way to make a class abstract when it has no other pure virtual function, and it is rarely what you want — if a class has no abstract behaviour, ask whether it should be a base at all.
11. When to Make a Base Abstract
Almost always, if it is a polymorphic base. The question is really the reverse — when should a polymorphic base have working implementations?
Three honest cases. A default that is correct for most implementations and genuinely optional to replace — a name() returning "unnamed", say. A non-virtual function built on the pure virtual ones, like §2's runFor, which is not a default at all. And a partial implementation in a middle layer (§5), which is still abstract.
Everything else is Chapter 17.1 §7's "interface that was never abstract" — a base that was meant as a contract and shipped as a suggestion.
12. What to Actually Do
Make every polymorphic base abstract unless you have one of §11's three reasons.
Write = 0 on every operation an implementation must supply.
Keep virtual ~T() = default — abstractness and destruction are independent.
Hold abstract types by reference or through an owning smart pointer — measured, both are ordinary.
Use std::vector<std::unique_ptr<Base>>, never std::vector<Base>.
Put a non-virtual public function in front of the pure virtual ones where the sequence is fixed — Chapter 17.9.
And read the two error messages as the same rule: an object would have had to be created.
13. Mistakes Worth Making Once
Shipping a base with working implementations when you meant a contract. A missing override becomes a wrong answer.
Expecting a half-implemented derived class to compile. Measured: it is abstract too, same message.
Believing you cannot name an abstract type by value. Measured: void f(Model m); and Model h(); both compile.
std::vector<Base>. Declaring it is fine; the first element construction is the error.
Forgetting the virtual destructor because the class is abstract. Unrelated constructs.
Thinking an abstract class has no size. Measured: 8 bytes — it still has a table pointer.
A pure virtual destructor without an out-of-line definition. Legal to declare, still needs a body.
Treating = 0 as a safety feature rather than as a way to state a contract.
14. Interview Relevance
"What is a pure virtual function?" A virtual function declared with = 0 and no implementation in that class; a class with one is abstract. The strong answer adds that = 0 is syntax, not an assignment.
"What can you not do with an abstract class?" Create an object of it — directly, by value as a parameter or return, or as a container element. Everything by reference or pointer works normally.
"What if a derived class implements only some of them?" It is abstract too, and the error appears at the first attempt to create one.
"Does an abstract class have a size?" Yes — measured 8 bytes, the table pointer.
"Does it still need a virtual destructor?" Yes. Independent constructs.
"What does making a base abstract do about object slicing?" It makes it a compile error — there is no base object to slice into. This is the answer that stands out, because most candidates treat slicing as something you avoid by discipline.
"When would a polymorphic base have a working implementation?" A genuinely optional default, a non-virtual function built on the pure virtual ones, or a partial middle layer.
15. What You Should Be Able to Say Now
- A base with working functions supplies a default, not a contract — a forgotten override becomes a wrong answer.
= 0makes a function pure virtual; one of them makes the class abstract. It is syntax, not an assignment.- Measured:
is_abstract<Model>=1,is_abstract<ReferenceModel>=0,is_default_constructible<Model>=0. - Measured:
Model m;givesvariable type 'Model' is an abstract class. - Measured: a derived class implementing none of them is abstract too, with the same message.
- Abstract classes may appear mid-hierarchy, narrowing a contract without completing it.
- Measured, four positions: a by-value declaration compiles, and so does an abstract return type; the definition and the call are errors.
- The rule is about creating an object, not naming the type — hence variable and allocating in the messages.
- Measured:
std::vector<Model>declares fine and fails atresize(1). - Measured: references,
Model&dispatch, andstd::unique_ptr<Model>ownership all work normally. - Measured:
sizeof(Model)is 8 — an abstract class still carries a table pointer. - A non-virtual public function may call pure virtual ones — the shape Chapter 17.9 argues for.
- Measured:
Model m = r;is a compile error, so an abstract base eliminates slicing by construction. - An abstract class still needs
virtual ~T()— the constructs are independent. - A pure virtual destructor is legal and still needs a definition.
- Make polymorphic bases abstract by default, with three narrow exceptions.
16. Exercises
Exercise 1 — write one. Define a Sink interface with write(const char*) and flush() as pure virtual, plus whatever else it needs.
Exercise 2 — the traits. For your Sink and one implementation, give is_abstract and is_default_constructible for both.
Exercise 3 — the refusals. Give the diagnostic for each: creating a Sink; a derived class implementing only write; std::vector<Sink> then resize(1); void log(Sink s) { }.
Exercise 4 — the nuance. Say which two of void f(Sink);, void f(Sink) {}, f(s);, Sink g(); compile, and state the underlying rule.
Exercise 5 — what still works. Write three ways of using a Sink that compile, and say why each is allowed.
Exercise 6 — the slicing connection. Explain why an abstract base makes Sink s = fileSink; impossible, and what that prevents.
Exercise 7 — the size. Explain why sizeof an abstract class is not zero.
Exercise 8 — defaults. Give one operation in your Sink that could reasonably have a default implementation and one that must not, with reasons.
Exercise 9 — the middle layer. Design a three-level hierarchy where the middle class is abstract, and say what it narrows.
17. Summary
- A working base function is a default;
= 0makes it a contract. - Measured:
is_abstractis a real type property, and an abstract class is not default-constructible. - Measured refusals: creating the base, a half-implemented derived class, a by-value definition or call, and constructing a container element — in two messages, both about creating an object.
- Measured nuance: a by-value declaration and an abstract return type both compile.
- Measured: references, dispatch through
Model&, andunique_ptr<Model>all work normally, andsizeofis 8. - Measured, the headline:
Model m = r;is a compile error — an abstract base eliminates object slicing by construction. - It still needs a virtual destructor — independent constructs.
- Make polymorphic bases abstract by default.
A contract now exists and cannot be ignored, and every listing in this module has quietly carried one more line whose purpose has been deferred three times. virtual ~Model() = default has appeared in every base class since Chapter 17.2, with a promise that the next chapter but two would explain it. This is that chapter. The failure is the first on this module's list of three, it is the one most likely to be in a working codebase right now, and the measurement is unusually clean: the same program, the same derived object, deleted through a base pointer with and without one word, counting allocations and frees. What makes it worth a chapter of its own rather than a rule to memorise is what happens when you reach for a smart pointer to avoid thinking about it — because one of the two obvious choices rescues you and the other does not, the difference is not where anyone would look for it, and the compiler's warning about the whole business points at a line inside a standard library header rather than at your code.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
