Virtual Methods and Inheritance
In Ch04, we discussed vtables and how they solve the problem of storing duplicate function pointers in every object. We also saw that manual vtable assignment is error-prone.
In Ch05, we learned how C++ automates resource management with constructors and destructors.
Now let’s see how C++ automates dynamic dispatch using virtual methods and
code reuse using inheritance.
Recall: Our Manual vtable in C
// Step 1: Define the vtable structure
struct payload_vtable {
void (*process)(const struct payload *self);
void (*destroy)(const struct payload *self);
};
// Step 2: Create static vtables (one per type)
const struct payload_vtable vtable_command_login = {
.process = process_command_login,
.destroy = destroy_command_login
};
const struct payload_vtable vtable_message = {
.process = process_message,
.destroy = destroy_message
};
// Step 3: Payload stores a vtable pointer
struct payload {
const struct payload_vtable *vtable;
union payload_data data;
};
// Step 4: Manual assignment in constructor
void parse_payload(struct payload *p, const char *raw) {
if (raw[0] == '/') {
// ... parsing logic
p->vtable = &vtable_command_login; // Manual!
} else {
p->vtable = &vtable_message; // Manual!
}
}
// Step 5: Dynamic dispatch through vtable
void process_next(struct payload_buffer *buf) {
struct payload *p = &buf->payloads[buf->process_base];
p->vtable->process(p); // Dynamic dispatch
}
Recall: Problems of manual vtables
- Manual vtable assignment. Easy to get wrong!
- No compile-time checks, type-safety. Can assign wrong vtable!
- Repetitive vtable creation for every type.
- No shared code between similar types.
C++ Syntactic Sugar: Virtual Methods!
Basic Virtual Method Syntax:
class Payload {
public:
// Virtual method = automatically uses vtable
virtual void process() {
cout << "Processing generic payload" << endl;
}
virtual ~Payload() = default;
};
class Message : public Payload {
public:
// Override base class method
void process() override {
cout << "Processing message" << endl;
}
};
What C++ Does Behind the Scenes:
// Compiler generates vtable automatically
struct Payload_vtable {
void (*process)(Payload *self);
void (*destructor)(Payload *self);
};
const struct Payload_vtable vtable_Message = {
.process = Message_process, // Automatically assigned!
.destructor = Message_destructor
};
struct Payload {
const struct Payload_vtable *vptr; // Hidden! Added by compiler
// ... your data members
};
// Constructor automatically sets vptr
void Message_constructor(struct Message *m) {
m->vptr = &vtable_Message; // Automatic assignment!
}
Unlike manual vtables:
- Compiler generates vtable structs
- Compiler populates vtable entries
- Compiler inserts vptr into every object
- Constructor automatically assigns correct vtable
overridekeyword provides compile-time checking
Inheritance: Sharing Code
The Problem with Repetition
In our C implementation, we had lots of repeated code:
void process_command_login(const struct payload *p) {
// Command-specific processing
printf("Command: login\n");
// ... print arguments
}
void process_command_join(const struct payload *p) {
// Command-specific processing
printf("Command: join\n");
// ... print arguments
}
void process_command_logout(const struct payload *p) {
// Command-specific processing
printf("Command: logout\n");
// No arguments
}
Every command has similar structure but different details.
Inheritance Solution
Base class contains shared behavior:
class Command : public Payload {
public:
Command(const char *command_name_)
: command_name { command_name_ } {}
// Shared implementation
void process() override {
cout << "Command: " << command_name << endl;
process_arguments(); // Call derived class method
}
virtual ~Command() {}
protected:
// Derived classes must implement this
virtual void process_arguments() = 0; // Pure virtual, check end of this
// README for details
// Syntax is not so important. When you are not sure about syntax, Google
// it.
string command_name;
};
Derived classes implement specific behavior:
class LoginCommand : public Command {
public:
LoginCommand(const char *username_, const char *password_)
: Command { "login" }, username { username_ }, password { password_ } {}
private:
// Implement required method
void process_arguments() override {
cout << " Arguments: [username: " << username
<< ", password: " << password << "]" << endl;
}
string username;
string password;
};
class JoinCommand : public Command {
public:
JoinCommand(const char *channel_)
: Command { "join" }, channel { channel_ } {}
private:
void process_arguments() override {
cout << " Arguments: [channel: " << channel << "]" << endl;
}
string channel;
};
class LogoutCommand : public Command {
public:
LogoutCommand() : Command { "logout" } {}
private:
void process_arguments() override {
cout << " Arguments: []" << endl;
}
};
// Polymorphism: store different types in same pointer type
Payload *payloads[3];
payloads[0] = new LoginCommand { "alice", "pass123" };
payloads[1] = new JoinCommand { "general" };
payloads[2] = new LogoutCommand {};
// Dynamic dispatch - each calls correct method
for (int i = 0; i < 3; i++) {
payloads[i]->process(); // Calls through vtable!
}
// Cleanup
for (int i = 0; i < 3; i++) {
delete payloads[i]; // Virtual destructor ensures correct cleanup!
}
Command: login
Arguments: [username: alice, password: pass123]
Command: join
Arguments: [channel: general]
Command: logout
Arguments: []
Syntax Reference
class Base {
// Shared functionality
};
class Derived : public Base {
// Specific functionality + inherits from Base
};
virtual return_type method_name(); // Can be overridden
- Pure Virtual Functions (Abstract Classes)
virtual void method_name() = 0; // MUST be overridden
- Override Keyword
void process() override; // Compile error if not overriding
- Virtual Destructor
virtual ~ClassName() {} // Critical for polymorphic classes!
Why critical? Without virtual destructor:
Payload *p = new Message { ... };
delete p; // Only calls ~Payload(), leaks Message members!
With virtual destructor:
Payload *p = new Message { ... };
delete p; // Calls ~Message() then ~Payload() - correct order!
class Example {
public:
// Accessible from anywhere
protected:
// Accessible from this class and derived classes
private:
// Accessible only from this class
};
You are now familiar with how C++ automates virtual table construction. Next:
- Implement command hierarchy. Create base
Commandclass and derived classes:LoginCommand,JoinCommand, andLogoutCommand. - Implement message hierarchy. Create base
Messageclass with:DirectMessage,GroupMessage, andGlobalMessage. Use inheritance to share message content storage and common processing logic. - Create polymorphic buffer that stores
Payload *pointers (base class pointers) and can hold any derived type (commands or messages). - Test virtual destruction via adding print statements in destructors to verify derived destructor runs first and base destructor runs second. (This task not implemented in provided solution to make code easier to read.)
virtual is syntactic sugar for the vtable pattern you built manually.
Inheritance is syntactic sugar for sharing vtable entries and data. C++ did not
invent polymorphism, it automated the plumbing.
On next chapter, we will continue our journey with references and copy management.
Pure Virtual Methods
In our Paload, Command, and Message examples, we used a specific syntax:
virtual void <method_name>() = 0;. This is called a pure virtual method.
A pure virtual method is a declaration that a function must exist, but the base class provides no implementation for it. It acts as a mandatory contract: any class that inherits from Payload or Command is required to implement this method to be considered complete.
When a class contains at least one pure virtual method, it becomes an Abstract Class.
- No Instantiation: You cannot create an object of an abstract class (e.g.,
new Command("login")will result in a compiler error). - Incomplete Blueprint: The class exists solely to provide a common interface and shared data for its children.
Under the Hood: The C Parallel
In your manual C vtable implementation, a pure virtual method is like defining
a function pointer in your vtable struct but purposefully leaving it
unassigned in the base “type”.
In C: If you accidentally called a NULL: function pointer, your program would
crash at runtime.
In C++: The compiler prevents this crash by refusing to compile any code that tries to create an object that hasn’t fulfilled its pure virtual requirements.
As you work through Ch06, notice that if you forget to implement
process_arguments() in classes that inherit Command, or process_recipient()
in classes that inherit Message, your code will not compile. This is the
“syntactic sugar” of compile-time safety replacing manual runtime checks.