155. Register Operator Overloading

You are implementing firmware that manipulates a 32-bit memory-mapped control register.

To improve readability and safety, all register bit manipulation must be expressed using operator overloading, not raw bitwise expressions scattered throughout the firmware logic.

You are given a partially implemented program.
The program flow and logic are fixed.

Your task is to modify only the Register32 class so that the program:

  • Compiles successfully
  • Applies all register operations correctly
  • Produces the expected output for all test cases

Input / Program Flow:

The program receives:

  • An integer N — number of operations
  • N lines, each describing an operation:
    • SET x → set bit x
    • CLEAR x → clear bit x

The initial register value is always 0.

 

Required Behavior:

  • Apply all operations in order
  • Print the final register value as an unsigned integer

Output Format:

Print exactly:

reg=<final_value>

 

Example

Input:

3
SET 0
SET 3
CLEAR 0 

Explanation:

  • Initial register value = 0
  • SET 0 → register becomes 0000 0001 (decimal 1)
  • SET 3 → register becomes 0000 1001 (decimal 9)
  • CLEAR 0 → register becomes 0000 1000 (decimal 8)

Output:

reg=8

 

Constraints:

  • The register is exactly 32 bits wide
  • Bit index x is guaranteed to be in the range 0–31
  • All input is guaranteed valid — no runtime validation is required
  • You must not modify main()
  • You must not modify control flow
  • The following lines must remain unchanged:

    reg |= (1u << bit); 
    reg &= ~(1u << bit);
  • You may only modify the Register32 class
  • No runtime conditionals inside the register abstraction
  • No virtual functions
  • No inheritance
  • No heap allocation
  • No STL containers
  • Operator behavior must be resolved entirely at compile time

 

 

 

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.