Lesson 10 / 14
Abstract Classes and Interfaces
The measurement of how a pure virtual function guarantees that no instance can be constructed from the base class, and that an interface is a pure contract carrying no data.
Published Updated
In this lesson
- Explains how a pure virtual function prevents an object from being constructed directly from the base class
- Measures that even an inherited function with a body from the base class always works with the derived object
- Separates the difference between an abstract class and a data-free interface class with an example
Contents
The previous lesson showed that Shape::introduce had a body, and could be called
even though derived classes did not redefine it. This lesson covers the case where
a base class is built entirely from functions with no body at all, declaring only a
contract: a pure virtual function (marked with = 0). No object can be
constructed directly from a class carrying such a function — only from derived
classes that fill it in completely. The measurement question is the same: which
object does a call through a base pointer or reference work with — but this time
the base type has no instance of its own at all.
No Object Can Be Constructed from the Base Class, Only from Derived Ones
Vehicle’s same_as function is pure virtual (= 0); this turns constructing an
object directly from Vehicle into a compile error — Vehicle v; cannot be
written. Only classes like Car and Bicycle that fill in same_as can have
objects constructed. Does each element in a Vehicle* array still reach the
particular object it itself points to?
// abstract_base.cpp -- no instance of a class carrying a pure virtual function can be constructed, only of those that fill it in
#include <iostream>
class Vehicle {
public:
virtual ~Vehicle() = default;
virtual bool same_as(const void* address) const = 0;
};
class Car : public Vehicle {
public:
bool same_as(const void* address) const override {
return static_cast<const void*>(this) == address;
}
};
class Bicycle : public Vehicle {
public:
bool same_as(const void* address) const override {
return static_cast<const void*>(this) == address;
}
};
int main() {
Car c;
Bicycle b;
Vehicle* vehicles[2] = { &c, &b };
std::cout << std::boolalpha;
std::cout << "vehicles[0] same as its own object c "
<< vehicles[0]->same_as(&c) << "\n";
std::cout << "vehicles[1] same as its own object b "
<< vehicles[1]->same_as(&b) << "\n";
std::cout << "vehicles[0] same as b (cross-check) "
<< vehicles[0]->same_as(&b) << "\n";
return 0;
}
vehicles[0] same as its own object c true vehicles[1] same as its own object b true vehicles[0] same as b (cross-check) false
PS7. If Vehicle v; were written, the compiler would reject it — the pure
virtual same_as leaves Vehicle incomplete on its own, only classes like Car
and Bicycle that completely fill in the function can be constructed. This means
the only thing a Vehicle* can point to is the instance of a concrete class that
fills in same_as — there is no possibility at all of falling back to an
instance of the base class itself, a “spare” instance. vehicles[0]->same_as(&c)
is true, vehicles[1]->same_as(&b) is true, the cross-check is false:
each element reaches its own concrete object. (M08/K02 asked which type resolved
the call; here, since the base type has no instance of its own at all, resolution
always falls to a concrete type — and just as an interface reference in Java can
never point to an object of the pure interface type itself, a pure virtual function
in C++ provides the same restriction.) The class is self: the call necessarily
works with a concrete derived object, because the base type itself can never exist.
An Inherited Body Also Works with the Same Object
Alongside the pure virtual same_as, Vehicle also carries a maintenance_info
with a body. Car does not redefine it, Bicycle does. When the inherited body
runs, does this still carry the address of the called object?
// abstract_inherited_function.cpp -- an inherited function with a body also carries the address of the called object
#include <iostream>
class Vehicle {
public:
virtual ~Vehicle() = default;
virtual bool same_as(const void* address) const = 0;
virtual void maintenance_info(const void* expected) const {
std::cout << "Vehicle::maintenance_info running object same as expected "
<< std::boolalpha << (static_cast<const void*>(this) == expected) << "\n";
}
};
class Car : public Vehicle {
public:
bool same_as(const void* address) const override {
return static_cast<const void*>(this) == address;
}
};
class Bicycle : public Vehicle {
public:
bool same_as(const void* address) const override {
return static_cast<const void*>(this) == address;
}
void maintenance_info(const void* expected) const override {
std::cout << "Bicycle::maintenance_info running object same as expected "
<< std::boolalpha << (static_cast<const void*>(this) == expected) << "\n";
}
};
int main() {
Car c;
Bicycle b;
Vehicle* v1 = &c;
Vehicle* v2 = &b;
v1->maintenance_info(&c);
v2->maintenance_info(&b);
return 0;
}
Vehicle::maintenance_info running object same as expected true Bicycle::maintenance_info running object same as expected true
PS8. Since Car does not redefine maintenance_info, the call
v1->maintenance_info(&c) runs the body of Vehicle::maintenance_info — but even
inside this body, this compared against c’s address returns true. Since
Bicycle supplies its own body, v2->maintenance_info(&b) runs
Bicycle::maintenance_info, and this is again b’s address. An inherited body
running does not mean Vehicle has an instance of its own — that body is only ever
run on a concrete Car object. The class is self: even the common body
supplied by the base class works with no object other than the concrete object it
was called on.
An Interface: Only a Contract, No Shared Data
Vehicle carried no data, but it did carry a common body — it was a full abstract
class. Printable is different: it has no data field, no function with a body at
all, only a contract. Report fills in this contract and carries its own data.
// interface_contract.cpp -- an interface carries no data at all, the one filling in the contract carries its own data
#include <iostream>
#include <string>
#include <utility>
class Printable {
public:
virtual ~Printable() = default;
virtual bool same_as(const void* address) const = 0;
};
class Report : public Printable {
public:
explicit Report(std::string title) : title_(std::move(title)) {}
bool same_as(const void* address) const override {
return static_cast<const void*>(this) == address;
}
const std::string& title() const { return title_; }
private:
std::string title_;
};
void list_item(const Printable& p, const void* expected) {
std::cout << std::boolalpha
<< "object called inside list_item same as expected "
<< p.same_as(expected) << "\n";
}
int main() {
Report r("Annual Report");
std::cout << "the report's title " << r.title() << "\n";
list_item(r, &r);
return 0;
}
the report's title Annual Report object called inside list_item same as expected true
PS9. Printable declares no data field at all; title_ belongs entirely to
Report — Printable never touches this data, it only imposes the same_as
contract. When r is bound to a Printable& and passed into list_item,
p.same_as(&r) is again true: reference binding does not construct a copy here
either, p is the same object as r inside main. The difference between
Vehicle and Printable is concrete: Vehicle can carry data and offer functions
with a common body (abstract class), Printable is purely signatures, no data, no
body at all (interface). The class is self: in both cases, the call always
works with the concrete object that fills in the contract.
A Pure Virtual Function Can Have a Body, But Only Through a Qualified Call
= 0 makes a function pure virtual and prevents the carrying class from being
instantiated — but this does not mean the function can never have a body.
NoisyVehicle::warn is both pure virtual and has a body. Is this body reached
through an ordinary virtual call, or some other way?
// abstract_pure_virtual_body.cpp -- a pure virtual function can have a body, but it only runs through a qualified call
#include <iostream>
class NoisyVehicle {
public:
virtual ~NoisyVehicle() = default;
virtual void warn() const = 0;
};
void NoisyVehicle::warn() const {
std::cout << "NoisyVehicle::warn (default body) ran\n";
}
class Truck : public NoisyVehicle {
public:
void warn() const override {
std::cout << "Truck::warn ran, now calling the base body through a qualified call\n";
NoisyVehicle::warn();
}
};
int main() {
Truck t;
NoisyVehicle& base_ref = t;
base_ref.warn();
return 0;
}
Truck::warn ran, now calling the base body through a qualified call NoisyVehicle::warn (default body) ran
PS28. base_ref.warn() is an ordinary virtual call — and since Truck
redefines warn, it never reaches NoisyVehicle’s body on its own, just like every
override in the previous lessons. NoisyVehicle::warn’s body only runs when
manually called with a qualified call (NoisyVehicle::warn();) from inside
Truck::warn — this line bypasses virtual dispatch and targets that name directly.
“Pure virtual” and “bodiless” diverge here: = 0 is what prevents the class from
being instantiated and forces derived classes to fill in the function; whether it
has a body is a separate question, and if it does, that body is reachable only
through a qualified call, not ordinary virtual dispatch. The class is self:
even the qualified call works on t itself, only name resolution determines which
body runs.
No Instance of an Abstract Base Can Be Constructed, But a Concrete Base Can Be Taken by Value and Sliced
Because Vehicle carries a pure virtual function, Vehicle v; never compiles at
all — this means a value of type Vehicle can also never exist, so the risk of
object slicing cannot even arise for Vehicle. How does the situation change when
a base class is not abstract, and has a full body?
// abstract_concrete_base_by_value.cpp -- no instance of an abstract base can be constructed, but a concrete base can be taken by value and sliced
#include <iostream>
#include <string>
class VehicleRecord {
public:
explicit VehicleRecord(std::string plate) : plate_(std::move(plate)) {}
std::string plate() const { return plate_; }
protected:
std::string plate_;
};
class DetailedRecord : public VehicleRecord {
public:
DetailedRecord(std::string plate, int km)
: VehicleRecord(std::move(plate)), km_(km) {}
int km() const { return km_; }
private:
int km_;
};
int main() {
DetailedRecord detailed("34ABC123", 15000);
VehicleRecord sliced = detailed;
std::cout << std::boolalpha;
std::cout << "sliced and detailed carry the same address "
<< (static_cast<void*>(&sliced) == static_cast<const void*>(&detailed)) << "\n";
std::cout << "sliced object's plate " << sliced.plate() << "\n";
return 0;
}
sliced and detailed carry the same address false sliced object's plate 34ABC123
PS29. VehicleRecord carries no pure virtual function, it is a concrete class
— so VehicleRecord sliced = detailed; compiles and produces the same slicing
measured in PS21 in the Inheritance lesson: sliced is a separate object living at
its own address, independent of detailed (false), and the km_ field
DetailedRecord added is not carried over at all. Vehicle has no counterpart to
this line, because defining a variable of type Vehicle is itself a compile error
— no Vehicle value that could be the target of slicing can ever exist.
Abstractness means not only “the obligation to redefine,” but also “a value of the
base type can never come into existence”: an abstract base can be accessed by
pointer or reference, but can never be placed by value into a variable, parameter,
or return value — this also makes it structurally immune to slicing. The slicing
risk exists only for concrete classes. The class is copy: sliced is an
independent copy of detailed’s base part.
The Role of an Interface: Handling Unrelated Types Under One Contract
Report and Invoice do not derive from each other, they share no data or
behavior at all — their only common point is filling in the Printable contract.
Can these two be processed uniformly, in the same vector, without their concrete
types being known?
// interface_role_multiple_types.cpp -- different concrete types can be processed uniformly through the same interface
#include <iostream>
#include <string>
#include <vector>
class Printable {
public:
virtual ~Printable() = default;
virtual std::string text() const = 0;
};
class Report : public Printable {
public:
explicit Report(std::string title) : title_(std::move(title)) {}
std::string text() const override { return "Report: " + title_; }
private:
std::string title_;
};
class Invoice : public Printable {
public:
explicit Invoice(int amount) : amount_(amount) {}
std::string text() const override { return "Invoice: " + std::to_string(amount_); }
private:
int amount_;
};
int main() {
Report r("Annual Report");
Invoice i(150);
std::vector<Printable*> documents{ &r, &i };
for (const auto* doc : documents) {
std::cout << doc->text() << "\n";
}
return 0;
}
Report: Annual Report Invoice: 150
PS30. Report and Invoice are not related in the same hierarchy tree, they
share no data field or function with a body — their only bond is that both fill in
Printable. The documents vector holds both under a single Printable* type,
and the loop calls text() without ever knowing the element’s concrete type; each
call still reaches the correct concrete type’s function: “Report: Annual Report”
and “Invoice: 150”. This shows the real role of an interface: unlike an abstract
class, an interface does not establish kinship, it only imposes a contract by
signature — every type that fills in the contract can be processed in the same
collection, the same loop, even with no other commonality between them. The class
is self: every text() call works with the very concrete object the element in
the loop points to.
An Interface Also Needs a Virtual Destructor
Although Printable appears to offer only a contract, being able to delete a
concrete derived object like Report through a Printable* still requires its own
destructor to be virtual. Does this lesson confirm that?
// interface_virtual_destructor.cpp -- an interface's virtual destructor runs the full destruction chain of the derived object
#include <iostream>
class Printable {
public:
virtual ~Printable() { std::cout << "Printable destructor\n"; }
virtual void print() const = 0;
};
class Report : public Printable {
public:
~Report() override { std::cout << "Report destructor\n"; }
void print() const override { std::cout << "Report::print\n"; }
};
int main() {
Printable* doc = new Report();
doc->print();
std::cout << "-- deletion begins --\n";
delete doc;
return 0;
}
Report::print -- deletion begins -- Report destructor Printable destructor
PS31. doc’s static type is Printable*, but delete doc runs Report
destructor first, then Printable destructor — the same principle measured as
PS24 in the Virtual Functions and Vtables lesson, here in the interface context.
Even though Printable is a contract offering only pure virtual functions, if its
destructor were not virtual, that same delete doc would call only
Printable’s own destructor, Report’s resources never being released — and with
the static and dynamic types differing, deleting through a non-virtual destructor
would be undefined behavior. This branch, as in PS24, is not run here because it
would not give a deterministic output. Conclusion: “offering only a contract” does
not exempt a class from the virtual-destructor requirement — if polymorphic
deletion through a base pointer is going to happen, the destructor must be virtual,
interface included. The class is self: the destruction chain always operates on
the real Report object doc points to.
The Bounding Measurement
The abstractness measured in this lesson is under single inheritance: each
concrete class filled in exactly one abstract base class or interface. The case
where a class derives from multiple base classes at once, filling in multiple
contracts simultaneously — multiple inheritance — is outside this lesson’s scope;
the next lesson measures that. That a pure virtual function can also have a body
was shown with only one example (NoisyVehicle::warn); which designs really need
that body was not generalized. That a line like Vehicle v; really produces a
compile error was not shown by running it in this lesson, because validation only
runs code that compiles successfully — this is a fact stated in the lesson text but
outside the code that was run.
Summary
- No object can be constructed directly from a class carrying a pure virtual
function (
= 0); only from derived classes that fill in that function. - Each element in a
Vehicle*array reaches its own concrete object, because the base type has no instance of its own at all — the class is self. - Even an inherited function with a body supplied by the base class works with no object other than the concrete object it was called on.
- An abstract class (
Vehicle) can carry data and offer functions with a common body; an interface (Printable) is purely signatures, carrying no data, no body at all. - An object passed into an interface reference is the same object inside the function as it is outside — reference binding does not construct a copy here either.
Next Step
In this lesson, every concrete class filled in exactly one abstract base class or interface; the base subobject always stood at the head of the derived object, just like in the Inheritance lesson. The next lesson, Multiple Inheritance and the Diamond Problem, measures the case where a class derives from more than one base class at once — there, the second and subsequent base subobjects may no longer stand at the head of the derived object, their addresses may not coincide with the derived object’s address. What is measured is again which object an expression works with, but this time which part of the derived object the base subobject stands in is also part of the question.
To keep your progress and take notes, Log in
My notes
Log in to take notes.