157. Polymorphism-ii

Question.6

A developer measures the cost of a virtual function call versus a direct call.

What is the typical overhead on ARM Cortex-M?

Need Help? Refer to the Quick Guide below

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:

  1. Runtime (Dynamic): Uses virtual functions. Flexible, but has slight overhead.
  2. 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.

  1. V-Table (Flash): The compiler creates a static table of function pointers for every class.
  2. vptr (RAM): Every object gets a hidden pointer (vptr) pointing to its class's V-Table.
StructureCost (Overhead)
RAM4 bytes per object (for the vptr).
FlashOne table per Class (not per object).
CPU2-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)

PitfallDetails
❌ Missing Virtual DestructorCRITICAL: 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 (Base*) or References (Base&).

void func(Base b) -> Slices the object (removes derived data).

void func(Base& b) -> Keeps polymorphism.

override KeywordAlways 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 ConstructorNever 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.

 

 

 

 

Select Answer

Restart quiz!