- References
- Function Overloading
- Default Function Arguments
- Inline Function
- Dynamic Memory Allocation
- Placement New
- nullptr
- Namespaces
- Type Aliases
- Enum classes
- constexpr
- static_assert
- mutable Keyword
- auto Keyword
- Smart Pointers
- Basics of Classes
- Constructors
- Destructors
- Operator Overloading
- Copy Semantics
- Move Semantics
- Composition, RAII & Ownership
- Inheritance
- Polymorphism
- Abstraction
- Encapsulation
- Template
- Static Memory
- Friend Function
- this Pointer
- Function Pointer
- Lambdas and Callback Management
- Union
Polymorphism (Flexible Interfaces)

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:
- Runtime (Dynamic): Uses
virtualfunctions. Flexible, but has slight overhead. - Compile-Time (Static): Uses Overloading and Templates. Zero overhead, but less flexible.
Syntax & Usage (Runtime Polymorphism)
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
}
};How It Works: The V-Table
Dynamic polymorphism isn't magic; it uses a lookup table.
- V-Table (Flash): The compiler creates a static table of function pointers for every class.
- vptr (RAM): Every object gets a hidden pointer (
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). |
Syntax & Usage (Static Polymorphism)
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!
}
Relevance in Embedded/Firmware
1. Hardware Abstraction Layer (HAL)
This is the standard use case. You define an ISerial interface. Your application uses ISerial.
- Board A uses
STM32UART. - Board B uses
NordicUART. - Unit Tests use MockUART. You can swap hardware without changing a single line of application logic.
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).
Common Pitfalls (Practical Tips)
| 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. |
Concept understood? Let's apply and learn for real