C++ / INHERITANCE AND POLYMORPHISM
Abstract classes and pure virtual interfaces
Declare pure virtual functions, know why abstract classes cannot be instantiated, and design interfaces that derived classes must complete.
What you will learn
- Write = 0 to require an override instead of supplying a default implementation
- Recognise that a class stays abstract until every inherited pure virtual is overridden
- Put shared algorithm code in an abstract base that calls its own pure virtual hooks
- Name abstract types only through pointers or references, never as values
Understanding Abstract classes and pure virtual interfaces
The = 0 after a virtual function declaration is not an assignment of any kind; it is a marker saying this class declares the operation but refuses to define it. Any class with at least one such unimplemented operation is abstract, and the compiler will not let you create an object of it: not as a local, not as a data member by value, not as an array element, not through new. The reason is direct rather than arbitrary. An object of that class would expose a member function callers are entitled to invoke, and there is nothing to run, so the language rejects the request for an object instead of waiting for the call.
Abstract does not mean empty. An abstract class may hold data members, define constructors that derived classes are forced to call, and implement ordinary member functions, including virtual ones that carry a usable default. That combination gives a shape worth reaching for: a public non-virtual function in the base expresses the fixed part of an algorithm and delegates only the varying steps to its own pure virtuals. Callers get a single entry point, and each derived class supplies just the decisions that genuinely differ.
What people call an interface in C++ is the extreme case of this: an abstract class with no data, nothing but pure virtual functions and a virtual destructor. Abstractness is a per-class property that is recomputed for every derived class, so if you override three of four pure virtuals your class is still abstract, and the diagnostic appears at the first attempt to instantiate it rather than at the missing override. Since a value of an abstract type can never exist, polymorphic code refers to it as Base& or unique_ptr<Base>, which is why interfaces show up in parameter lists and members but never as return-by-value types.
<cstddef>
<iostream>
<memory>
<string>
<vector>
class Compressor {
public:
virtual ~Compressor() = default;
// No sensible generic implementation exists: every format differs.
virtual std::string name() const = 0;
virtual std::string pack(const std::string& data) const = 0;
// Concrete code living in the abstract base, phrased via the pure virtuals.
void report(const std::string& data) const {
std::string out = pack(data);
std::cout << name() << ": " << data.size() << " -> " << out.size()
<< " bytes (" << out << ")\n";
}
};
class RunLength : public Compressor {
public:
std::string name() const override { return "rle"; }
std::string pack(const std::string& data) const override {
std::string out;
for (std::size_t i = 0; i < data.size();) {
std::size_t j = i;
while (j < data.size() && data[j] == data[i]) ++j;
out += data[i];
out += std::to_string(j - i);
i = j;
}
return out;
}
};
class Identity : public Compressor {
public:
std::string name() const override { return "none"; }
std::string pack(const std::string& data) const override { return data; }
};
int main() {
// Compressor c; // error: cannot declare an object of abstract type
std::vector<std::unique_ptr<Compressor>> pipeline;
pipeline.push_back(std::make_unique<RunLength>());
pipeline.push_back(std::make_unique<Identity>());
for (const auto& c : pipeline)
c->report("aaabbbbcc");
}
A pure virtual function states a requirement rather than a behaviour, and a class with any unfulfilled requirement is usable as a type but impossible as an object.
Worked examples
A pure virtual function with a body
Shows that = 0 controls the override requirement, not whether an implementation exists, and that a derived class can stay abstract.
<iostream>
class Logger {
public:
virtual ~Logger() = default;
virtual void write(const char* msg) = 0;
virtual void flush() = 0;
};
// A pure virtual function may be defined, but only out of line.
void Logger::write(const char* msg) {
std::cout << "[base] " << msg << "\n";
}
class Timestamped : public Logger { // still abstract: flush() not overridden
public:
void write(const char* msg) override {
std::cout << "12:00 ";
Logger::write(msg); // reuse the shared part
}
};
class Console : public Timestamped {
public:
void flush() override { std::cout << "[flushed]\n"; }
};
int main() {
// Timestamped t; // error: flush() is still pure here
Console c;
Logger& l = c;
l.write("ready");
l.flush();
}
Example explained
Line 1virtual void write(const char*) = 0; demands an override; the separate definition below only adds reusable code.
Line 2void Logger::write(...) must be written outside the class body, since = 0 and an inline body cannot be combined.
Line 3Timestamped overrides write but inherits flush as pure, so it is abstract and no Timestamped object can exist.
Line 4Logger::write(msg) is a qualified call, so it skips dispatch and runs the base body instead of recursing into itself.
Checking when a class becomes concrete
Uses std::is_abstract_v to see abstractness recomputed at each level of a hierarchy, while a reference to the abstract type stays legal.
<iostream>
<type_traits>
struct Validator {
virtual ~Validator() = default;
virtual bool check(int v) const = 0;
virtual const char* why() const = 0;
};
struct Positive : Validator {
bool check(int v) const override { return v > 0; }
// why() left alone, so it stays pure
};
struct PositiveMsg : Positive {
const char* why() const override { return "must be > 0"; }
};
int main() {
std::cout << std::boolalpha;
std::cout << "Validator abstract: " << std::is_abstract_v<Validator> << "\n";
std::cout << "Positive abstract: " << std::is_abstract_v<Positive> << "\n";
std::cout << "PositiveMsg abstract: " << std::is_abstract_v<PositiveMsg> << "\n";
PositiveMsg v;
const Validator& ref = v; // naming an abstract type is fine
std::cout << ref.check(-3) << " " << ref.why() << "\n";
}
Example explained
Line 1Validator is abstract because two of its members are declared = 0 and never defined in that class.
Line 2Positive overrides one of the two, and the untouched why() remains pure, so Positive is abstract as well.
Line 3PositiveMsg supplies the last override, which is exactly what flips is_abstract_v to false and makes PositiveMsg v; legal.
Line 4const Validator& ref = v; creates no Validator object, so the abstract type may appear here as a name for the contract.
Important notes
A destructor may be pure virtual, but it must still be given a body out of line (Base::~Base() {}), because every derived destructor calls it.
C++ has no interface keyword; an interface is only a convention, namely an abstract class with no data members and nothing but pure virtual functions plus a virtual destructor.
Common mistakes
Writing virtual void f() = 0 { ... } with the body inside the class: this never compiles, because the definition of a pure virtual must be given separately as void Base::f() { ... }.
Missing one override, or writing a signature that differs by a const or a parameter type so nothing is overridden: the derived class silently remains abstract, and the error surfaces as cannot declare variable of abstract type at the first attempt to create it, far from the real mistake.
Calling a pure virtual from the base constructor or destructor: no derived override is in effect at that moment, so the program aborts at runtime with pure virtual method called instead of reaching the derived version.
Try it yourself
Change, predict, then run
Write an abstract class Tax with pure virtual rate() and label(), plus a non-virtual apply(double amount) that returns amount * (1 + rate()). Implement Vat (0.2) and ZeroRate (0.0), then print label() and apply(50.0) for each through a Tax& and confirm that Tax t; fails to compile.
Open the C++ workspaceCheck your understanding
A base declares virtual double area() const = 0; and also provides an out-of-line definition double Shape::area() const { return 0.0; }. What does that definition change?
- Nothing about the rules: Shape is still abstract and derived classes must still override area(); the body runs only via an explicit Shape::area() call
- Shape stops being abstract, because area() now has an implementation, so Shape objects can be created
- The program is rejected, because a pure virtual function is not allowed to have a definition
- Derived classes may skip area(), since dynamic dispatch will fall back on the base body
Show answer
The = 0 marker, not the presence of a body, is what makes a class abstract and forces overrides; a definition merely gives derived code something to reuse through a qualified call. Option 4 is tempting because a body is normally the target of dispatch, but a derived class that omits area() keeps the pure declaration and is itself abstract, so no call ever lands on Shape::area() implicitly.