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
overridingspeak()
with “Moo!”. - Create
void makeItSpeak(const Animal& a)
that callsa.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 satisfyILogger
. - Create
Circle
andRectangle
derived fromShape
and override botharea()
andname()
.
🔹 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
), addoverride
, 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 togreet("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 withsave()
and implement it inPlayer
. - 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!