Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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

  • = 0 makes 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 Validator holding Validators — works only with a narrow interface.

Questions about this lesson

Log in to ask a question.