In today’s fast-changing world of development, it is important to write code that is understable, maintainable, scalable, and stable. That means your code should be easy to understand, maintain, and grow over time.
In C++, one useful feature that helps us achieve this is the pure virtual function. This feature allows us to create a clear structure in our programs, making the code more organized and easier to test. This article will explain what pure virtual functions are, why they are useful, and how to use them correctly.
What is a 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.
It is declared using the = 0 syntax:
The syntax of the pure virtual function is as follows:
virtual <Function_Type> <Function_Name>(Passed_Parameters) = 0;
Example,
class IUart
{
public:
// Pure virtual function
virtual void transmitData() = 0;
};
The above declaration tells the compiler that the class is abstract. A class with at least one pure virtual function is called an abstract class, which means it cannot be instantiated directly. So, you must implement the pure virtual function in any derived class.
Why Use Pure Virtual Functions?
In projects, we often work with different hardware modules or communication protocols. To make our code more flexible and organized, we use abstraction. This means focusing on what a component should do, not how it does it.
Pure virtual functions help with this. They let us create interfaces in base classes that define the required functions. Then, each specific hardware or module can provide its own version of how those functions work. This makes the code easier to update, test, and reuse across different devices.
Example,
Suppose you are writing a driver framework for different types of EEPROM devices (I2C, SPI, etc.):
Below implemented class (IEEPROM) is an interface for EEPROM memory. It defines two pure virtual functions: read() and write(), but does not provide any actual code for them.
This means any class that inherits from IEEPROM—like I2CEEPROM or SPIEEPROM must provide its own version of these functions based on how that type of EEPROM works (e.g., I2C or SPI).
class IEEPROM
{
public:
virtual void read(uint32_t addr, uint8_t* buffer, size_t len) = 0;
virtual void write(uint32_t addr, const uint8_t* data, size_t len) = 0;
virtual ~IEEPROM() = default;
};
This way, your main application doesn’t need to care about which type of EEPROM is used. It just calls read() and write(), and the right thing happens in the background 😊.
Benefits in Firmware Architecture:
Here are a few points from my personal experience that show why pure virtual functions are a key part of our system design:
1. Encapsulation of Hardware Details:
Each hardware device works differently, but the system doesn’t need to know if the EEPROM uses I2C or SPI. Pure virtual functions help hide those details and keep the code clean.
2. Testability:
It helps us in unit testing. For unit testing, we can use mock classes that override the pure virtual functions. This allows us to test higher-level logic without needing actual hardware.
3. Design Scalability:
It is easy to replace hardware using interfaces without changing the main application. Just create a new class from the base interface and add the required functions.
4. Compile-Time Safety:
The compiler ensures that all pure virtual functions are implemented in derived classes — reducing the risk of runtime errors.
⚠️ Common Mistakes:
It is very important to understand to when and how to use the virtual function. Sometimes even experienced engineers can run into issues with virtual functions. Here are some common pitfalls that I have seen over the years:
| Mistake | What Happens | Our Recommendation |
|---|---|---|
| Forgetting to override a pure virtual function. | Compilation error | Use override keyword for clarity |
| Instantiating an abstract class. | Compilation error | Always instantiate derived classes only |
| Defining unnecessary pure virtual destructors without body. | Linker error (undefined reference to destructor) | If virtual destructor is pure, give it a body:virtual ~Base() = 0;and define it separately |