In embedded firmware systems, hardware drivers are often accessed through a common interface so that different peripherals can be managed uniformly.
This relies on runtime polymorphism, where derived driver implementations override virtual functions defined in a base driver interface.
However, passing polymorphic objects by value causes object slicing, which removes the derived portion of the object and breaks runtime dispatch.
Your task is to correct the given program so that runtime polymorphism is preserved, and the correct derived driver behavior executes when processed through the base interface.
You must fix the object slicing issue without redesigning the driver class hierarchy.
Input / Program Flow
Driver selection rules:
0, use the SPI driver1, use the I2C driverProgram flow:
Output
If input is 0, output:
SPI driver runningIf input is 1, output:
I2C driver runningOutput Requirements
Constraints
new, delete)
Polymorphism ("Many Forms") allows you to treat different objects (like TempSensor, PressureSensor) as a single generic type (Sensor). It decouples the Interface (what a function does) from the Implementation (how it does it).
In Embedded C++, it comes in two flavors:
virtual functions. Flexible, but has slight overhead.1. Virtual Functions
The virtual keyword tells the compiler: "Don't bind this function call yet. Wait until the program runs to see what type of object this actually is."
class Protocol {
public:
// Virtual: Can be overridden by children
virtual void send(uint8_t data) {
printf("Generic Send\n");
}
};
class UART : public Protocol {
public:
// Override: Replace generic logic with UART logic
void send(uint8_t data) override {
UART_HW->DR = data;
}
};
class SPI : public Protocol {
public:
void send(uint8_t data) override {
SPI_HW->DR = data;
}
};
// The Magic: This function works for UART, SPI, I2C, etc.
void transmit_data(Protocol* p, uint8_t byte) {
p->send(byte); // Calls the specific version automatically
}
2. Abstract Classes (Interfaces)
In HAL design, the Base class often has no logic—it just defines the API. We use Pure Virtual Functions (= 0) to enforce this.
A class with pure virtual functions cannot be instantiated.
// Interface (Contract)
class IDigitalOut {
public:
virtual void write(bool state) = 0; // Pure Virtual
virtual ~IDigitalOut() {} // Virtual Destructor (Required)
};
// Implementation
class GpioPin : public IDigitalOut {
public:
void write(bool state) override {
// Hardware specific code
}
};Dynamic polymorphism isn't magic; it uses a lookup table.
vptr) pointing to its class's V-Table.| Structure | Cost (Overhead) |
|---|---|
| RAM | 4 bytes per object (for the vptr). |
| Flash | One table per Class (not per object). |
| CPU | 2-3 extra cycles (Fetch pointer -> Jump). |
1. Function Overloading
Same function name, different arguments. Resolved at compile time.
void log(int i);
void log(char* s);
// Compiler picks the right one. Zero overhead.
2. Templates (CRTP)
Used when you want polymorphic behavior (generic drivers) but cannot afford the RAM cost of V-Tables.
template <typename T>
void send_byte(T& driver, uint8_t b) {
driver.send(b); // Resolved at compile time!
}
1. Hardware Abstraction Layer (HAL)
This is the standard use case. You define an ISerial interface. Your application uses ISerial.
STM32UART.NordicUART.2. Heterogeneous Collections
You can iterate over a list of different devices.
// Array of pointers to the Base class
Sensor* sensors[] = { &temp, &imu, &battery };
for (auto* s : sensors) {
s->read(); // Calls Temp::read(), then IMU::read()...
}3. Testability (Mocking)
Polymorphism is essential for Unit Testing on a PC.
You pass a MockGPIO (which just prints to screen) into your driver instead of a real RealGPIO (which touches registers).
| Pitfall | Details |
|---|---|
| ❌ Missing Virtual Destructor | CRITICAL: If a Base class has virtual functions, the Destructor must be virtual. If not, delete base_ptr will not call the Derived destructor, causing resource leaks. |
| ❌ Object Slicing | Polymorphism only works with Pointers (
|
❌ override Keyword | Always use override in derived classes. If you accidentally change the signature (send(int) vs send(uint8_t)), override triggers a compile error. Without it, you create a generic bug. |
| ❌ Calling Virtual in Constructor | Never call a virtual function inside a Constructor or Destructor. The object is not fully formed yet, so it will call the Base version, not the Derived version. |