Question.7
A developer uses a template instead of virtual for zero-overhead polymorphism:
template<typename Driver>
void send_byte(Driver& drv, uint8_t b) {
drv.send(b); // Resolved at compile time
}
UART uart;
SPI spi;
send_byte(uart, 0x55);
send_byte(spi, 0x55);How does this differ from virtual dispatch?
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. |