Skip to content
C++
Language
since C++98
Basic

Abstract Classes and Pure Virtual Functions

C++ abstract classes — pure virtual functions, NVI pattern, covariant return types, override/final, and virtual destructor semantics.

Abstract Classsince C++98

A class with at least one pure virtual function (= 0) that cannot be instantiated directly, establishing an interface contract that concrete derived classes must fulfill.

Overview

An abstract class defines a polymorphic interface without committing to an implementation. The compiler rejects any attempt to construct one directly. A derived class that provides definitions for all inherited pure virtual functions becomes concrete and instantiable; one that leaves even a single pure virtual unimplemented remains abstract.

Abstract classes fall on a spectrum:

  • Pure interface — only pure virtual functions and a virtual destructor; no data, no non-pure members
  • Partial implementation — pure virtuals mixed with concrete methods and data members
  • Non-Virtual Interface (NVI) — non-virtual public methods delegating to private virtual hooks

C++ has no interface keyword. The contract is enforced entirely through = 0, access specifiers, and the override/final specifiers introduced in C++11.

Syntax

cpp
class Base {
public:
    virtual ReturnType method(Params) qualifiers = 0;  // pure virtual — makes class abstract
    virtual ~Base() = default;                          // C++11: = default
};

// Optional out-of-class body — class remains abstract; body reachable only via Base::method()
ReturnType Base::method(Params) qualifiers { /* default implementation */ }

A pure virtual function may carry an out-of-class definition. The = 0 still makes the class abstract and still demands that derived classes override; the body is only reachable through an explicit qualified call (Base::method()). This pattern provides a sensible fallback without relaxing the override requirement.

override (C++11) instructs the compiler to verify that the function signature matches a virtual in a base. Without it, a signature mismatch silently creates a new, non-overriding overload — a frequent and silent bug. final (C++11) on a virtual function prevents further overriding; applied to the class itself, it prevents subclassing.

Examples

Minimal interface

cpp
#include <cstdio>
#include <string_view>  // C++17

class Logger {
public:
    virtual void write(std::string_view msg) = 0;
    virtual void flush() = 0;
    virtual ~Logger() = default;  // C++11
};

class StdoutLogger : public Logger {
public:
    void write(std::string_view msg) override {   // C++11: override
        std::fwrite(msg.data(), 1, msg.size(), stdout);
        std::fputc('\n', stdout);
    }
    void flush() override { std::fflush(stdout); }
};

Pure virtual with a default body

cpp
#include <span>    // C++20
#include <vector>
#include <cstddef>

class Codec {
public:
    virtual std::vector<std::byte> encode(std::span<const std::byte> in) const = 0;  // C++20
    virtual ~Codec() = default;
};

// Identity fallback — class is still abstract
std::vector<std::byte> Codec::encode(std::span<const std::byte> in) const {
    return {in.begin(), in.end()};
}

class ZlibCodec : public Codec {
public:
    std::vector<std::byte> encode(std::span<const std::byte> in) const override {
        if (in.empty()) return Codec::encode(in);  // explicit delegation to base body
        // ... actual zlib compression ...
        return {};
    }
};

Non-Virtual Interface (NVI) pattern

NVI separates the public API from the extension point. The non-virtual public method owns invariants — validation, logging, metrics — while the private virtual hook is the subclass's sole responsibility:

cpp
#include <numeric>
#include <span>   // C++20

class Processor {
public:
    bool process(std::span<const int> data) {   // non-virtual public API
        if (data.empty()) return false;
        return do_process(data);                 // dispatch to private hook
    }
    virtual ~Processor() = default;

private:
    virtual bool do_process(std::span<const int> data) = 0;
};

class SumProcessor : public Processor {
    int result_ = 0;

    bool do_process(std::span<const int> data) override {
        result_ = std::accumulate(data.begin(), data.end(), 0);
        return true;
    }
public:
    int result() const { return result_; }
};

NVI is generally preferred over all-public pure virtual interfaces: the base retains control over the call site, and preconditions or instrumentation can be added later without touching the public API or any existing callers.

Covariant return types

An override may return a more-derived pointer or reference type — the only case where the return type is allowed to differ:

cpp
class Node {
public:
    virtual Node* clone() const = 0;
    virtual ~Node() = default;
};

class LeafNode : public Node {
    int value_;
public:
    explicit LeafNode(int v) : value_{v} {}
    LeafNode* clone() const override {  // covariant: LeafNode* narrows Node*
        return new LeafNode{*this};
    }
};

Callers holding a LeafNode* get back a LeafNode* without a cast; callers holding a Node* still work correctly through the base interface.

Compile-time checks

cpp
#include <type_traits>

static_assert(std::is_abstract_v<Logger>);       // C++17: _v helper; C++11 used ::value
static_assert(!std::is_abstract_v<StdoutLogger>);

// C++20: concept-gated factory
template<typename T>
    requires std::derived_from<T, Logger> && (!std::is_abstract_v<T>)
std::unique_ptr<Logger> make_logger() {
    return std::make_unique<T>();
}

Best Practices

Always declare a virtual destructor. Deleting a derived object through a base pointer with a non-virtual destructor is undefined behaviour. If the class manages no resources, = default is sufficient. The rule applies even to pure interfaces that hold no data.

Mark every override with override. A signature mismatch — wrong const qualifier, wrong parameter type — compiles silently without override, producing a hidden new overload instead of an override. The compiler-enforced annotation eliminates the entire category.

Keep interfaces narrow. Clients pay for every vtable slot. Fat interfaces force implementers to stub out methods they will never use. Multiple small, cohesive interfaces compose better than one large one.

Prefer NVI over all-public pure virtual. NVI preserves the base class's ability to enforce invariants and add diagnostics without changing the public API or recompiling callers.

Restrict data to partial-implementation bases. A pure interface with data members complicates multiple inheritance by introducing separate subobjects per base. When a diamond occurs, virtual inheritance becomes necessary — use it deliberately, not accidentally.

Common Pitfalls

Non-virtual destructor in a polymorphic base. GCC and Clang warn only under -Wnon-virtual-dtor (not enabled by -Wall). Runtime symptoms include resource leaks or heap corruption. The fix is always to add virtual ~Base() = default;.

Signature mismatch without override. void process(int x) const override {} catches that the base declared void process(int x) {} (non-const); without override, both silently coexist and the intended dispatch never happens.

Calling a pure virtual from a constructor or destructor. During base construction, the vtable pointer resolves to the base class. Calling a pure virtual that has no body results in a link error; calling one that does dispatches to the base body, not the derived override — almost never the intended behaviour.

cpp
class Bad {
public:
    Bad() { init(); }       // dispatches to Bad::init during construction, NOT Derived::init
    virtual void init() = 0;
    virtual ~Bad() = default;
};

Partially overriding in a deep hierarchy. A class that overrides some but not all inherited pure virtuals remains abstract. Compilers report which pure virtuals are missing, but in a multi-level hierarchy the origin can be non-obvious.

Slicing through base references. Abstract classes cannot be sliced by construction, but partial-implementation bases with data can be assigned by reference in ways that lose the derived state. Protect or delete copy/move in any base that should not be treated as a value type.

Pure Virtual Destructor

Why a destructor can be pure virtual

Making the destructor pure virtual is the idiomatic way to force a class to be abstract when you have no other pure virtual functions to declare. The result is the simplest possible abstract class — one with a guaranteed-abstract base and no mandatory method contract beyond destruction:

cpp
class AbstractBase {
public:
    virtual ~AbstractBase() = 0;  // pure virtual destructor
};

// Definition REQUIRED — even though the function is pure virtual
AbstractBase::~AbstractBase() {}

The = 0 makes AbstractBase uninstantiable. The out-of-class definition is not optional — it is mandatory. When a derived object is destroyed, its destructor runs first and then the compiler implicitly calls every base-class destructor up the chain. If AbstractBase::~AbstractBase has no body, the linker reports an unresolved symbol error because the implicit call from the derived destructor has nowhere to go.

This stands in contrast to any other pure virtual function: calling a pure virtual non-destructor from outside a constructor or destructor (other than via explicit Base::f() syntax) is undefined behaviour, but the destructor is called unconditionally by the runtime.

The "abstract with no methods" pattern

When you need a base that is uninstantiable — to mark ownership hierarchies, restrict direct use of a middle layer, or tag a family of types — but you do not want to impose any method contract, a pure virtual destructor is the right tool:

cpp
// Registry tag: only subtypes of PluginBase can be registered.
// No methods mandated beyond proper destruction.
class PluginBase {
public:
    virtual ~PluginBase() = 0;
protected:
    PluginBase() = default;  // constructible only by derived classes
};

PluginBase::~PluginBase() {}

class AudioPlugin : public PluginBase {
public:
    void render(float* buf, std::size_t n);
    ~AudioPlugin() override = default;
};

The protected constructor prevents accidental on-stack construction even in the same translation unit.

Inline definition (compiler extension)

MSVC accepts an inline body on a pure virtual destructor (virtual ~Base() = 0 {}). GCC and Clang do not: the = 0 and a brace-body on the same declaration are ill-formed outside a Microsoft extension. Stick to the out-of-class definition for portability.

= 0 With a Body

Any pure virtual function — not just the destructor — may carry an out-of-class definition. The = 0 still makes the class abstract and still requires derived classes to override; the body is simply reachable via an explicit qualified call:

cpp
class Serializer {
public:
    // Pure virtual: Serializer cannot be instantiated;
    // every derived class must override serialize().
    virtual std::string serialize(const void* data, std::size_t size) const = 0;
    virtual ~Serializer() = default;
};

// Optional fallback — returns a hex dump
std::string Serializer::serialize(const void* data, std::size_t size) const {
    std::string out;
    out.reserve(size * 2);
    const auto* bytes = static_cast<const unsigned char*>(data);
    for (std::size_t i = 0; i < size; ++i) {
        char buf[3];
        std::snprintf(buf, sizeof buf, "%02x", bytes[i]);
        out.append(buf, 2);
    }
    return out;
}

class JsonSerializer : public Serializer {
public:
    std::string serialize(const void* data, std::size_t size) const override {
        // Attempt JSON encoding; fall back to hex on failure
        if (!data || size == 0)
            return Serializer::serialize(data, size);  // explicit call to base body
        // ... real JSON implementation ...
        return "{}";
    }
};

Use cases for this pattern:

  • Opt-in default behaviour — derived classes that do something reasonable in the common case call Base::f() rather than re-implementing it; exotic subclasses override fully.
  • Documentation by example — the base body shows what a correct implementation looks like without removing the override requirement.
  • Hook with sensible no-op — a notification hook (virtual void on_event() = 0;) can have an empty body so derived classes that need no action can call Base::on_event() instead of writing their own empty override.

override and final Specifiers (C++11)

override

override is a context-sensitive identifier (not a reserved keyword — it is legal as a variable name elsewhere, though inadvisable) that instructs the compiler to verify that the decorated function signature exactly matches a virtual function in a direct or indirect base. If no match exists, compilation fails:

cpp
class Base {
public:
    virtual void process(int x) const = 0;
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    // void process(int x) override {}         // ERROR: missing const — no matching virtual
    // void process(long x) const override {}  // ERROR: wrong parameter type
    void process(int x) const override {}      // OK
};

Without override, both incorrect signatures compile silently as new (non-overriding) virtual functions, and calls through a Base* always invoke the base's pure virtual — a runtime crash or link error.

final on functions

final on a virtual function prevents any further-derived class from overriding it. This is useful when an optimized implementation must not be replaced:

cpp
class CachedProcessor : public Base {
public:
    void process(int x) const override final {
        // This exact implementation is used for all further subclasses.
    }
};

class Specialized : public CachedProcessor {
    // void process(int x) const override {}  // ERROR: process is final in CachedProcessor
};

The compiler may devirtualize calls to final functions because the target is statically known — a tangible performance benefit in hot paths.

final on classes

Applied to the class itself, final prohibits subclassing entirely:

cpp
class Singleton final {
public:
    static Singleton& instance();
private:
    Singleton() = default;
};

// class MySingleton : public Singleton {};  // ERROR: Singleton is marked final

Both override and final are context-sensitive: they are ordinary identifiers that carry special meaning only in the specific grammar position after a declarator. Legacy code that uses them as variable or type names continues to compile unchanged.

Non-Virtual Interface (NVI) Pattern in Depth

Motivation

When every public function is pure virtual, derived classes control the entire call sequence. The base class cannot enforce preconditions, log calls, take locks, or add instrumentation later without modifying every override. NVI solves this by making the public API non-virtual and moving the extension point to a private (or protected) virtual:

cpp
class DataSource {
public:
    // Non-virtual public API — the base always owns this code path.
    std::vector<std::byte> read(std::size_t n) {
        if (n == 0) return {};          // precondition: meaningless to read zero bytes
        auto result = do_read(n);
        if (result.size() != n)
            throw std::runtime_error{"short read"};
        return result;
    }

    bool seek(std::int64_t offset) {
        if (!is_seekable()) return false;
        return do_seek(offset);
    }

    virtual bool is_seekable() const { return false; }
    virtual ~DataSource() = default;

private:
    virtual std::vector<std::byte> do_read(std::size_t n) = 0;
    virtual bool do_seek(std::int64_t) { return false; }
};

class FileSource : public DataSource {
    FILE* fp_;
public:
    explicit FileSource(FILE* fp) : fp_{fp} {}
    bool is_seekable() const override { return true; }
private:
    std::vector<std::byte> do_read(std::size_t n) override;
    bool do_seek(std::int64_t offset) override;
};

The precondition check and the short-read assertion in read() apply to every present and future DataSource implementation. Adding a logging call or a metrics increment to read() later requires touching only one place.

Relationship to Template Method

NVI is the C++ expression of the Template Method design pattern: the non-virtual public method is the "template" (fixed algorithm skeleton), and the private virtuals are the "steps" that vary. The difference from the classic Gang-of-Four formulation is that C++ NVI uses private virtuals rather than protected ones — Herb Sutter's original 2001 Guru of the Week article recommended private virtuals because derived classes should override behaviour, not call the hook directly. Protected virtuals are appropriate when derived classes legitimately need to call Base::do_foo() for chaining.

Access control and overriding

C++ allows a private virtual to be overridden in a derived class even though the derived class cannot call it directly — access control and override resolution are independent. This is the mechanism that makes private-virtual NVI work:

cpp
class Base {
    virtual void hook() = 0;  // private virtual
public:
    void run() { hook(); }    // calls via vtable — perfectly legal
};

class Derived : public Base {
    void hook() override {}   // overrides the private virtual — also legal
};

Abstract Classes and Constructors

Abstract classes do have constructors

An abstract class cannot be instantiated, but it is constructed as a subobject every time a derived class object is created. Its constructors are called normally and may perform arbitrarily complex initialization. The typical convention is to make them protected to document that direct construction is not the intent:

cpp
class Shape {
protected:
    explicit Shape(std::string_view name) : name_{name} {}
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    const std::string& name() const { return name_; }
    virtual ~Shape() = default;
private:
    std::string name_;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : Shape{"circle"}, radius_{r} {}
    double area() const override { return 3.14159265358979 * radius_ * radius_; }
    double perimeter() const override { return 2 * 3.14159265358979 * radius_; }
};

Do not call virtual functions from constructors or destructors

During base-class construction, the vtable pointer is set to the base class's vtable. The derived class's portion of the object does not yet exist, so calling a virtual function resolves to the base version — not the derived override. If the base version is pure virtual and has no body, the result is undefined behaviour (most implementations crash with a pure virtual call at runtime):

cpp
class Dangerous {
public:
    Dangerous() {
        configure();  // dispatches to Dangerous::configure, NOT any derived version
    }
    virtual void configure() = 0;   // no body — pure virtual call: undefined behaviour
    virtual ~Dangerous() = default;
};

class Safe {
public:
    Safe() = default;
    virtual void configure() = 0;
    virtual ~Safe() = default;

    // Factory method pattern: construct, then call configure() explicitly
    static std::unique_ptr<Safe> create(std::unique_ptr<Safe> obj) {
        obj->configure();  // now the vtable is fully set up
        return obj;
    }
};

The same rule applies to destructors: when the derived destructor has already run and the base destructor is executing, virtual calls resolve back to the base class. The design consequence is that any two-phase initialisation (init(), configure(), open()) must be called after construction is complete — either by the caller or via a factory.

Covariant Return Types

Rules

The C++ standard allows an overriding function to return a pointer or reference to a type that is more derived than the base virtual's return type. Both of the following must hold:

  1. The base virtual returns B* (or B&); the override returns D* (or D&), where D is publicly derived from B.
  2. The covariant type is accessible and unambiguous at the point of the override declaration.

Reference covariance follows the same rules — D& can covary with B&.

Clone pattern

Covariant return types shine in the clone() / prototype pattern, eliminating a static_cast at every call site:

cpp
class Document {
public:
    virtual Document* clone() const = 0;
    virtual ~Document() = default;
};

class SpreadsheetDoc : public Document {
    std::vector<std::vector<double>> cells_;
public:
    explicit SpreadsheetDoc(std::vector<std::vector<double>> c)
        : cells_{std::move(c)} {}

    SpreadsheetDoc* clone() const override {   // covariant: SpreadsheetDoc* narrows Document*
        return new SpreadsheetDoc{*this};
    }
};

void example(SpreadsheetDoc* doc) {
    // Covariant: no cast needed — clone() returns SpreadsheetDoc* here
    SpreadsheetDoc* copy = doc->clone();
    // Via base pointer: still works, returns Document* (base virtual's declared return type)
    Document* base_ptr = doc;
    Document* base_copy = base_ptr->clone();   // correct polymorphic clone
    delete copy;
    delete base_copy;
}

With std::unique_ptr the covariance is lost because unique_ptr<D> is not derived from unique_ptr<B>. The standard workaround is a non-virtual clone() returning unique_ptr<Derived> that calls a protected virtual do_clone():

cpp
class Document {
public:
    std::unique_ptr<Document> clone() const {
        return std::unique_ptr<Document>(do_clone());
    }
    virtual ~Document() = default;
protected:
    virtual Document* do_clone() const = 0;
};

class SpreadsheetDoc : public Document {
protected:
    SpreadsheetDoc* do_clone() const override {
        return new SpreadsheetDoc{*this};
    }
};

This restores type-safe memory management while keeping the covariant return type on the internal virtual.

See Also

  • Virtual Functions — vtable layout and dispatch mechanics
  • override and final — C++11 specifiers in depth
  • Type Erasure — runtime polymorphism without inheritance overhead
  • CRTP — static polymorphism as an alternative to virtual dispatch
  • Concepts — compile-time interface constraints (C++20)