Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close