I have designed a wide range of embedded systems devices, from precise energy meters to ultra-low-power IoT devices. One important lesson I have learned over the years is that code maintainability matters a lot. A maintainable code matters far more in the long run than clever performance tricks. In fact, clean, well-structured and maintainable code has a longer lifespan than the hardware it runs on.
In this article, you will learn about interfaces and abstract classes in C++ through clear, practical explanations (Interface vs Abstract Class in C++) . I will be sharing insights from real-world experience that I hope will help you avoid common pitfalls and save valuable time in your own projects.
Interface:
If a class is a blueprint for a physical object, then an interface is a blueprint for behavior. Interface defines what a class can do, but not how it does it.
An interface describes a set of actions an object must support, without specifying their internal details. In this way, it acts as a contract that any implementing class must fulfill.
Just as a class defines the structure of an entity, an interface defines its capabilities. In other words, “a class tells you what it has, while an interface tells you what it should be able to do“.
Let’s consider with a real-life example,
A class is like a blueprint of a device that includes a UART module.
It defines what the device has, such as:
- UART channel number.
- Configuration settings (baud rate, parity, start and stop bits).
- Transmit and receive buffers.
- Control registers, etc.
// Abstract Class: Defines what UART has (structure)
class UARTBase
{
protected:
uint32_t baudRate;
uint8_t channelNumber;
uint8_t stopBits;
uint8_t parity;
public:
UARTBase(baudRate baud,uint8_t ch, uint8_t stop, uint8_t p)
: baudRate(baud), channelNumber(ch), stopBits(stop),parity(p)
{
}
virtual ~UARTBase() {}
void configure()
{
// Setup UART with settings
}
};
It describes the structure and internal details of how UART is set up in a specific system.
An interface is like a contract or protocol — it defines what the device should be able to do, such as:
- readData() – receive data from UART
- writeData() – send data over UART
- startCommunication() – initialize or enable the UART module.
C++ doesn’t have a dedicated interface keyword like Java. So, in C++, an interface is typically a class composed entirely of pure virtual functions. It has no state and no implementation—just a set of behaviors that implementing classes must fulfill.
// Interface: Defines what UART should be able to do
class IUARTInterface
{
public:
virtual ~IUARTInterface() {}
virtual void startCommunication() = 0;
virtual void writeData(char data) = 0;
virtual char readData() = 0;
};
It doesn’t specify how these actions are done — only that they must be provided by any class that implements the interface.
This separation of behavior from implementation makes code more modular, maintainable, and flexible—allowing different parts of a program to work together without depending on specific implementations. Consider the implementation of UART1.
// Concrete UART class that implements the interface and extends the base
class UART1 : public UARTBase, public IUARTInterface
{
public:
UART1() : UARTBase(1, 9600, 'N', 1) {}
void startCommunication() override
{
configure();
// Start UART hardware
}
void writeData(uint8_t data) override
{
// Send data (example)
}
uint8_t readData() override
{
uint8_t data;
// Read data (example)
return data;
}
};
Key Characteristics of an Interface in C++:
Here are some important characteristics of an interface class in C++:
- All member functions are pure virtual (= 0).
- Ideally, it should not have any data members.
- Cannot be instantiated directly.
- Enables polymorphism and loose coupling.
Why Use Interfaces in Firmware Development?
It helps us in building robust, maintainable, and scalable software. Let’s understand why experienced firmware engineers love to use the interfaces as much as possible.
1. Decouples Implementation from Usage:
Interfaces allow higher-level code to interact with hardware or services without knowing how they are implemented. It promotes loose coupling and cleaner separation of concerns.
🔹Example:
An IUARTInterface lets your application send and receive data, without needing to know how the UART implemented whether it uses polling, DMA, or interrupts.
2. Enables Hardware Abstraction:
Interfaces help you build Hardware Abstraction Layers (HALs) by defining what hardware should do, not how. It Improves portability across platforms and simplifies hardware upgrades.
🔹Example:
You can have the same IUARTInterface implemented for different hardware targets (e.g., Renesas, STM32, NXP, or a simulated UART for testing).
It saved a lot of time in my project where we were replacing the target MCU. We only needed to write a new driver wrapper for the new MCU. Also, we were even able to reuse the existing unit test cases without writing new ones.
3. Facilitates Unit Testing & Mocking:
As mentioned, interfaces are essential in test-driven development for mocking hardware behavior. This allowed us to swap out the MCU with minimal effort and reuse all existing tests. It enables testing business logic without real hardware.
🔹 Example:
Instead of depending on actual hardware during testing, you can inject a mock UART that implements the same IUARTInterface.
4. Supports Dependency Injection:
Interfaces allow injecting different implementations at runtime or compile-time, making code more flexible and reusable. Dependency injection is one of the techniques to achieve dependency inversion of the princpile of SOLID. It promotes flexible system configuration.
🔹 Example:
An embedded application can use IUARTInterface* pUart and choose the appropriate driver at boot time.
5. Improves Code Reusability and Maintainability:
If you define a common interface, you can create multiple interchangeable implementations. This reduces code duplication, simplifies maintenance, and makes future upgrades much cleaner.
🔹 Example:
By defining a standard interface for external flash memory, you can support various flash chips (e.g., Macronix, Winbond, etc.) using the same application-level code. The implementation can be swapped without changing how the code interacts with the flash device.
Abstract Class:
In C++, a class becomes abstract when it defines at least one pure virtual function. A pure virtual function is a virtual function that has no implementation in the base class and requires derived classes to override it.
Example:
virtual void startCommunication() = 0;
An abstract class not only contains the pure virtual function but despite this, an abstract class can still:
- Contain concrete (non-virtual) functions.
- Have data members.
- Provide partial implementations that can be reused by derived classes
Note: You cannot create objects from an abstract class directly. Instead, you use it as a base class to define a common interface for derived classes.
I think you might be wondering—what’s the point of using an abstract class?
Don’t worry I am explaining.
“An abstract class is useful when you want to share common code among multiple classes, but still force each class to provide its own specific behavior“.
Consider the below UART code.
class Uart
{
public:
Uart();
virtual ~Uart();
void RunCommunicationLoop(); // shared logic
// Implemented by the Derived class
virtual void Init(uint32_t baudRate) = 0;
virtual void WriteByte(uint8_t byte) = 0;
virtual uint8_t ReadByte() = 0;
protected:
void ProcessReceivedByte(uint8_t byte);
};
Derived class will handle the low-level initialization and read/write operation.
class StUart : public Uart
{
public:
void Init(uint32_t baudRate) override
{
// Set baud rate registers
}
void WriteByte(uint8_t byte) override
{
// Write to hardware register
}
uint8_t ReadByte() override
{
// Read from hardware register
return 0;
}
};
when to use an interface vs an abstract class in C++?
✅ Use an Interface When:
- You care about behavior, not implementation. That means Any object that implements this interface should behave like this, regardless of how it does so.”
- You need a strict contract with no assumptions about how it will be implemented.
- You want loose coupling.
- You want multiple implementations and polymorphism.
- You need testability or mocking.
- You don’t need shared data or default implementations.
✅ Use an Abstract Class When:
- You want to share common code among multiple child classes.
- You want to partially enforce behavior while still allowing flexibility for subclasses.
- You need to manage protected/shared data.
- You are designing a class hierarchy, such as a base Peripheral class.
Choosing Between Interface and Abstract Class in C++:
Still confused about when to use an interface or when to use an abstract class?
Don’t worry the quick-reference table below compares them across common scenarios to help you decide what is the best for your design.
The table highlights when to use interfaces or abstract classes by aligning their strengths with real-world software design needs.
| Use Case / Need | Interface ✅ | Abstract Class ✅ |
|---|---|---|
| You want to enforce a strict contract | ✔️ | ✔️ |
| You want to reuse code | ❌ | ✔️ |
| You want no state, just behavior | ✔️ | ❌ |
| You need multiple inheritance support | ✔️ | ⚠️ Risky |
| You want to mock or inject easily | ✔️ | ✔️ (less clean) |
| You need shared data or protected members | ❌ | ✔️ |
| You’re writing a hardware abstraction | ✔️ | ✔️ |
| You want test-driven design flexibility | ✔️ | ✔️ |
Note: Rule which I personally follow:
- Use interfaces when your goal is to describe capability.
- Use abstract classes when you want to share behavior.