150. Heap-Free Polymorphic Driver

In embedded firmware systems, dynamic memory allocation is often forbidden due to fragmentation risks and nondeterministic behavior.
Despite this restriction, firmware frequently requires runtime polymorphism to select and execute different driver implementations through a common interface.

You are given a small firmware-style driver framework with a base driver interface and two concrete drivers.
Your task is to implement runtime polymorphism without using heap allocation, ensuring safe and deterministic object lifetime.

You must select the correct driver at runtime and invoke its behavior through a base-class interface, while keeping all objects stack-allocated.

Input / Program Flow

  • One integer value is read from standard input.

Driver selection rules:

  • If the input value is 0 → select the SPI driver
  • If the input value is 1 → select the I2C driver

Program flow:

  1. Read one integer value from input
  2. Create both driver objects on the stack
  3. Select the appropriate driver at runtime
  4. Store the selected driver using a base-class reference
  5. Invoke the driver function polymorphically

Output

  • If input is 0, output:

    SPI transfer started
    
  • If input is 1, output:

    I2C transfer started
    

Output Requirements

  • Exactly one line of output
  • Output text and spacing must match exactly
  • Output must be produced via a polymorphic call
  • No extra output or blank lines

Constraints

  • Language standard: C++11
  • Dynamic memory allocation is forbidden
    • Do NOT use new, delete, malloc, or free
  • Do NOT use STL containers
  • Do NOT change existing function names
  • Polymorphism must be preserved
  • Object lifetime must remain valid and deterministic

 

 

 

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.