Pure Virtual Function in C++: Abstract Classes and Examples

A Pure Virtual Function in C++ is a virtual function declared with = 0 that has no implementation in the base class and forces derived classes to provide their own. It makes the base class abstract, enabling clean, extensible polymorphism.

Welcome! If you’re just starting with C++, this guide will make Pure Virtual Function in C++ clear and practical. You’ll see commented code, outputs, and short “Try it yourself” challenges after every section so you can practice confidently.

🔹 What is a Pure Virtual Function in C++?

A pure virtual function is declared in a base class with = 0, meaning the base class provides no implementation. Any class that contains at least one pure virtual function becomes an abstract class, which cannot be instantiated. Derived classes must override the function to be concrete and usable.

  • Makes the base class abstract (non-instantiable).
  • Forces derived classes to implement required behavior.
  • Enables runtime polymorphism through base pointers/references.
  • Often combined with a virtual destructor for safe cleanup.

Try it yourself

  • In one sentence, explain why a Pure Virtual Function in C++ creates a “contract.”
  • Think of three interfaces from daily life (Printer, Payment, Sensor) and list one mandatory function for each.

🔹 Basic Syntax and First Example

Declare a pure virtual function with = 0 in the base class, then implement it in derived classes using the same signature and the override keyword for safety.

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
// Abstract base: has a pure virtual function
class Animal {
public:
    virtual ~Animal() = default; // virtual destructor for polymorphic cleanup
    virtual void speak() const = 0; // pure virtual -> must be overridden
};
class Dog : public Animal {
public:
    void speak() const override { // implements the contract
        cout << "Woof!\n";
    }
};
class Cat : public Animal {
public:
    void speak() const override {
        cout << "Meow!\n";
    }
};
int main() {
    // Animal a; // ERROR: cannot instantiate abstract class (uncomment to test)
    vector<unique_ptr<Animal>> zoo;
    zoo.push_back(make_unique<Dog>());
    zoo.push_back(make_unique<Cat>());
    for (const auto& a : zoo) {
        a->speak(); // dynamic dispatch via pure virtual function in C++
    }
}

Output

Woof!
Meow!

Because speak() is pure virtual in Animal, every concrete animal must implement it. Calls through Animal* or Animal& resolve to the correct derived behavior at runtime.

Try it yourself

  • Add Cow overriding speak() with “Moo!”.
  • Create void makeItSpeak(const Animal& a) that calls a.speak() and pass different animals to it.

🔹 Abstract Classes and Interfaces

In C++, there’s no separate “interface” keyword. An interface is simply an abstract class whose functions are all pure virtual (plus a virtual destructor). Abstract classes can also include non-pure virtuals or helpers to share behavior.

// Interface-style abstract class: all pure virtual
struct ILogger {
    virtual ~ILogger() = default;
    virtual void log(const char* msg) = 0; // must be implemented
};
// Abstract class with a mix: pure + defaulted non-pure
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0; // pure virtual
    virtual const char* name() const { // default behavior (can be overridden)
        return "Shape";
    }
};

Choose an interface when you want only requirements. Choose an abstract class when you also want to share common behavior among derived types.

Try it yourself

  • Implement ConsoleLogger and (mock) FileLogger that satisfy ILogger.
  • Create Circle and Rectangle derived from Shape and override both area() and name().

🔹 Pure Virtual Destructor (Yes, that’s valid!)

You can declare a pure virtual destructor to make a class abstract, but it must still have a definition because destructors always run. This is useful when the class is intended as an interface only.

#include <iostream>
using namespace std;
class IFace {
public:
    virtual ~IFace() = 0; // pure virtual destructor (makes class abstract)
    virtual void run() = 0;
};
IFace::~IFace() { 
    // still must define it
    // cleanup common to all implementations (if any)
}
class Impl : public IFace {
public:
    void run() override {
        cout << "Impl::run\n";
    }
};
int main() {
    // IFace x; // ERROR: abstract
    Impl obj;
    obj.run();
}

Output

Impl::run

Marking the destructor pure virtual prevents direct instantiation, while the definition ensures destruction works correctly through base pointers.

Try it yourself

  • Add a side effect in IFace::~IFace() (e.g., print “IFace dtor”). Confirm it runs when deleting through base pointers.
  • Make another pure virtual function and implement it in Impl.

🔹 Signature Matching, const, and override

To override a pure virtual function, the signature must match exactly (name, parameters, and qualifiers like const). Use override so the compiler catches mistakes that would otherwise cause name hiding.

#include <iostream>
using namespace std;
struct Base {
    virtual void info() const = 0; // note: const
};
struct Bad : Base {
    // void info() { cout << "Bad\n"; } 
    // WRONG: missing const -> does NOT override
};
struct Good : Base {
    void info() const override { 
        cout << "Good\n"; 
    }
};
int main() {
    // Base b; // abstract
    Good g;
    g.info(); // OK
    // If Bad::info() (non-const) existed, Base* p = new Bad; p->info(); 
    // would call Base's impl (if any) or fail link.
}

Small qualifier differences (like missing const) silently break overriding. override prevents these bugs at compile time—always use it on derived virtuals.

Try it yourself

  • Create a mismatch on purpose (remove const), add override, and observe the helpful compiler error.
  • Experiment with reference qualifiers (&, &&) and see how they affect overriding.

🔹 Default Arguments and Pure Virtuals

Default arguments are bound at compile time to the static type of the expression. Even if the body dispatches virtually, the default parameter value comes from the static type, which can surprise you. Prefer explicit arguments or overloads instead.

#include <iostream>
using namespace std;
struct Base {
    virtual ~Base() = default;
    virtual void greet(const char* who = "Base") const = 0; // pure virtual with default
};
struct Derived : Base {
    void greet(const char* who = "Derived") const override {
        cout << "Hello from " << who << "\n";
    }
};
int main() {
    Derived d;
    Base& b = d;
    b.greet();           // Body: Derived::greet, Default arg: "Base" (static type)
    b.greet("Custom");   // Explicit arg avoids confusion
}

Output

Hello from Base
Hello from Custom

Guideline: Avoid default arguments on virtuals when working with a Pure Virtual Function in C++. Use explicit parameters or provide a no-arg overload that forwards a chosen value.

Try it yourself

  • Remove defaults and add an overload greet() that forwards a chosen name to greet("Derived").

🔹 Template Method Pattern with Pure Virtual Steps

The Template Method pattern defines a non-virtual algorithm in the base class that calls pure virtual “steps” implemented by derived classes. It enforces a flow yet allows customization.

#include <iostream>
#include <string>
using namespace std;
class DataPipeline {
public:
    virtual ~DataPipeline() = default;
    // Non-virtual algorithm: fixed flow
    void process(const string& src, const string& dst) {
        auto raw = read(src);           // virtual step
        auto cooked = transform(raw);   // virtual step
        write(dst, cooked);             // virtual step
    }
protected:
    virtual string read(const string& src) = 0;
    virtual string transform(const string& data) = 0;
    virtual void write(const string& dst, const string& data) = 0;
};
class UppercasePipeline : public DataPipeline {
protected:
    string read(const string& src) override {
        return "[raw] " + src;
    }
    string transform(const string& data) override {
        string out = data;
        for (char& c : out) c = (char)toupper((unsigned char)c);
        return out;
    }
    void write(const string& dst, const string& data) override {
        cout << "Writing to " << dst 
        << ": " << data << "\n";
    }
};
int main() {
    UppercasePipeline p;
    p.process("hello world", "out.txt");
}

Output

Writing to out.txt: [RAW] HELLO WORLD

Here, the base class owns the algorithm, while derived classes implement required steps via pure virtuals. This keeps the process consistent and flexible.

Try it yourself

  • Add a TrimPipeline that trims spaces before uppercasing.
  • Insert optional logging hooks in process without touching derived classes.

🔹 Multiple Inheritance with Pure Virtual Interfaces

C++ supports multiple inheritance, and it’s common to implement multiple pure virtual interfaces in a single class. Just implement all required functions with exact signatures.

#include <iostream>
using namespace std;
struct Drawable {
    virtual ~Drawable() = default;
    virtual void draw() const = 0;
};
struct Updatable {
    virtual ~Updatable() = default;
    virtual void update(double dt) = 0;
};
class Player : public Drawable, public Updatable {
public:
    void draw() const override {
        cout << "Player::draw\n";
    }
    void update(double dt) override {
        cout << "Player::update dt=" << dt << "\n";
    }
};
int main() {
    Player p;
    Drawable* d = &p;
    Updatable* u = &p;
    d->draw();
    u->update(0.016);
}

Output

Player::draw
Player::update dt=0.016

This pattern is handy for systems where an object must fulfill multiple roles, like game entities or GUI widgets, while staying decoupled via pure virtual interfaces.

Try it yourself

  • Add a Serializable interface with save() and implement it in Player.
  • Store and use pointers to each interface type for the same object.

🔹 Mini Project: Exporters with Pure Virtual Function in C++

Let’s build a simple, extensible export system. Each exporter implements the same pure virtual interface but writes data differently. Adding new formats won’t change the calling code.

#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;
class Exporter {
public:
    virtual ~Exporter() = default;
    virtual const char* name() const = 0;       // pure virtual
    virtual void write(const vector<string>& rows) = 0; // pure virtual
};
class CSVExporter : public Exporter {
public:
    const char* name() const override { return "CSV"; }
    void write(const vector<string>& rows) override {
        cout << "[CSV]\n";
        for (const auto& r : rows) cout << r << "\n";
    }
};
class JSONExporter : public Exporter {
public:
    const char* name() const override { return "JSON"; }
    void write(const vector<string>& rows) override {
        cout << "[JSON]\n[\n";
        for (size_t i = 0; i < rows.size(); ++i) {
            cout << " \"" << rows[i] << "\"";
            if (i + 1 != rows.size()) cout << ",";
            cout << "\n";
        }
        cout << "]\n";
    }
};
void exportAll(Exporter& ex, const vector<string>& rows) {
    cout << "Exporting via " << ex.name() << "...\n";
    ex.write(rows); // virtual call -> derived behavior
}
int main() {
    vector<string> rows = {"alpha,beta,gamma", "1,2,3", "x,y,z"};
    vector<unique_ptr<Exporter>> list;
    list.push_back(make_unique<CSVExporter>());
    list.push_back(make_unique<JSONExporter>());
    for (auto& e : list) exportAll(*e, rows);
}

Output

Exporting via CSV...
[CSV]
alpha,beta,gamma
1,2,3
x,y,z
Exporting via JSON...
[JSON]
[
 "alpha,beta,gamma",
 "1,2,3",
 "x,y,z"
]

The Exporter interface defines a stable contract. New exporters (XML, Markdown, etc.) can be added without changing exportAll or callers—just implement the pure virtuals.

Try it yourself

  • Add MarkdownExporter that prints each row as “- row”.
  • Change exportAll to count rows and print a summary after writing.

🔹 Best Practices and Common Pitfalls

  • Always provide a virtual (often defaulted) destructor in polymorphic bases.
  • Use override for every derived virtual function.
  • Keep interfaces small and focused; avoid “god” abstract classes.
  • Avoid default arguments on virtuals to prevent static-binding surprises.
  • Document contract expectations: preconditions, postconditions, and ownership.
  • Prefer composition over inheritance if “is-a” isn’t clear.

Try it yourself

  • Split an overly large interface into two smaller, cohesive ones.
  • Add unit tests that call through base pointers/references to verify correct overrides.

🔹 FAQs about Pure Virtual Function in C++

Q1: What makes a class abstract?
Having at least one Pure Virtual Function in C++ (declared with = 0) makes the class abstract and non-instantiable.

Q2: Can a destructor be pure virtual?
Yes, but it must still have a definition. This pattern enforces abstraction while ensuring proper cleanup.

Q3: Do signatures need to match exactly?
Yes. Names, parameters, and qualifiers like const must match. Use override so the compiler validates it.

Q4: Are default arguments compatible with virtuals?
They work, but defaults bind to the static type at compile time and can be surprising. Prefer explicit arguments or overloads.

Q5: Interface vs abstract class?
An “interface” is just an abstract class whose functions are all pure virtual (plus a virtual destructor). Abstract classes may also include default behavior or helpers.

Q6: Can I provide a default implementation for a pure virtual?
No. Pure virtual means “no base implementation.” You can provide non-pure virtuals with defaults alongside pure virtuals.

Q7: How do pure virtuals relate to templates?
Pure virtuals enable runtime polymorphism; templates enable compile-time polymorphism. Choose based on whether behavior is known at build time or runtime.

Q8: Should base destructors always be virtual?
Yes, if you delete derived objects through base pointers. It ensures correct destructor chaining and resource release.

Q9: Can access specifiers change on override?
Yes, you can widen access (e.g., protected → public), but do so thoughtfully as it changes your API contract.

Q10: Are pure virtuals slower?
Virtual calls add a tiny indirection (vtable lookup), but the overhead is negligible for most apps compared to the design benefits.

🔹 Wrapping Up

A Pure Virtual Function in C++ defines a must-implement contract that makes your base class abstract and your design extensible. Use = 0 for required behaviors, add a virtual destructor, and rely on override for safety. Practice the “Try it yourself” challenges to solidify your understanding—happy coding!

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.