Lesson 4 of 9
Shape::area() returning 0 was always a lie. A shape does not have a default area — the question
only makes sense for a specific shape.
A pure virtual function says exactly that: this operation exists in the interface and has no sensible base implementation. Every derived class must supply one.
Abstract classes
#include <iostream>
#include <memory>
#include <string>
#include <type_traits>
#include <vector>
// A pure virtual function makes the class ABSTRACT: it cannot be instantiated.
class Serializer {
public:
virtual ~Serializer() = default;
virtual std::string serialize(int value) const = 0; // pure: must override
virtual std::string format_name() const = 0;
// An abstract class may still have implemented members and data.
std::string describe(int value) const {
return format_name() + " -> " + serialize(value);
}
// A pure virtual function MAY have a body, called explicitly.
virtual void reset() = 0;
};
// Even a pure virtual can be defined, for derived classes to call.
void Serializer::reset() { std::cout << " (Serializer::reset base cleanup)\n"; }
class JsonSerializer : public Serializer {
public:
std::string serialize(int value) const override { return "{\"v\":" + std::to_string(value) + "}"; }
std::string format_name() const override { return "json"; }
void reset() override {
Serializer::reset(); // explicit call to the pure virtual's body
std::cout << " json state cleared\n";
}
};
class CsvSerializer : public Serializer {
public:
std::string serialize(int value) const override { return std::to_string(value); }
std::string format_name() const override { return "csv"; }
void reset() override { std::cout << " csv state cleared\n"; }
};
int main() {
std::cout << std::boolalpha;
std::cout << "Serializer is abstract: "
<< std::is_abstract_v<Serializer> << "\n";
std::cout << "JsonSerializer is abstract: "
<< std::is_abstract_v<JsonSerializer> << "\n\n";
std::vector<std::unique_ptr<Serializer>> formats;
formats.push_back(std::make_unique<JsonSerializer>());
formats.push_back(std::make_unique<CsvSerializer>());
for (const auto& f : formats) {
std::cout << f->describe(42) << "\n";
}
std::cout << "\nresetting:\n";
for (const auto& f : formats) {
f->reset();
}
return 0;
}
Serializer is abstract: true
JsonSerializer is abstract: false
json -> {"v":42}
csv -> 42
resetting:
(Serializer::reset base cleanup)
json state cleared
csv state cleared
= 0 after a virtual function makes it pure. A class with any pure virtual function is
abstract, which means:
// Serializer s; // error: abstract type
// std::vector<Serializer> v; // error: needs to construct one
//
// Serializer* p = new JsonSerializer(); // fine
// std::unique_ptr<Serializer> p = ...; // fine
// void use(const Serializer& s); // fine — references are fine
You cannot create one, but you can point to one or reference one — which is all polymorphism needs.
std::is_abstract_v confirms the distinction: Serializer is abstract, JsonSerializer is not,
because it overrode everything.
An abstract class is not an interface
This is the part people miss. An abstract class may contain:
Implemented functions. describe() above is a normal member that calls two pure virtuals. That
is the template method pattern: the base defines the algorithm's shape and the derived classes
fill in the steps.
Data members. Nothing stops an abstract class holding state, and a protected constructor to initialise it.
Constructors. They run when a derived object is built, exactly as in lesson 16.1.
A body for a pure virtual function. Serializer::reset() is both pure and defined. Derived
classes must override it, but they can call the base version explicitly for shared cleanup — as
JsonSerializer::reset does. This is unusual and occasionally exactly right.
A class with only pure virtual functions, no data, and a virtual destructor is what most languages call an interface. C++ has no keyword for it; it is just a convention:
// class Drawable {
// public:
// virtual ~Drawable() = default;
// virtual void draw(Canvas&) const = 0;
// virtual Rect bounds() const = 0;
// };
The destructor is not inherited as pure
// class Base {
// public:
// virtual ~Base() = default; // NOT pure — it must be callable
// virtual void f() = 0;
// };
A destructor can be pure, but then you must define it anyway, because the derived destructor calls it:
// virtual ~Base() = 0;
// Base::~Base() {} // required, or the link fails
The only reason to do that is to make a class abstract when it has no other pure virtual function. It is rare and there is usually a better candidate.
Why this beats a default implementation
Compare the two designs:
// A: a default that is wrong
// virtual double area() const { return 0.0; }
//
// A derived class that forgets to override compiles, runs, and silently
// reports zero. The bug appears as a wrong total, far from its cause.
//
// B: pure virtual
// virtual double area() const = 0;
//
// A derived class that forgets to override cannot be instantiated. The
// error is at compile time and names the missing function.
Design B turns a run-time wrong answer into a compile-time error. That is the same trade as lesson 12.7's concepts and lesson 15.1's exceptions over sentinel returns, and it is the reason to prefer pure virtual whenever there is no genuinely sensible default.
GCC's message when you forget:
error: invalid new-expression of abstract class type 'Triangle'
note: because the following virtual functions are pure within 'Triangle':
note: 'virtual double Shape::area() const'
It names the class and the function you missed.
Try it
Design an abstract Validator for form fields, with validate(value) returning an error message
or empty, and field_name(). Implement three validators, then add a CompositeValidator that
holds several and runs them all. Use the template method pattern to share the "report the first
failure" logic in the base.
Checkpoint
= 0makes a virtual function pure; any class with one is abstract.- An abstract class cannot be instantiated, but pointers and references to it are fine.
std::is_abstract_v<T>reports it at compile time.- An abstract class may have implemented functions, data, and constructors — it is not an interface.
- The template method pattern puts the shared algorithm in the base and the steps in derived classes.
- A pure virtual function may still have a body, callable explicitly as
Base::f(). - A destructor may be pure, but must then be defined anyway.
- Prefer pure virtual over a wrong default: it turns a run-time wrong answer into a compile error.
- GCC names the class and every unoverridden pure virtual when you try to instantiate.
- The composite pattern — a
ValidatorholdingValidators — works only with a narrow interface.
Questions about this lesson
Log in to ask a question.