Virtual Function in C++

In C++, virtual functions are a fundamental part of object-oriented programming that enable runtime polymorphism. This dynamic dispatch mechanism makes virtual functions essential for building flexible, extensible, and modular systems especially in large-scale or hardware-abstracted software architectures.

In this article you will learn what is virtual function in C++ and when to use it.  I will also share insights from real-world experience that I hope will help you avoid common pitfalls and save valuable time in your own projects.

 

What Is a Virtual Function in C++?

In C++, a virtual function is a member function that you define in a base class and expect to override in derived classes to provide specific behavior.

You declare “virtual function” using the virtual keyword in the base class. Virtual functions enable runtime polymorphism, which means the program chooses the correct function to run based on the actual object type—even when you access it through a base class pointer or reference.

Let’s see an example to understand the concept of the virtual function.

#include<iostream>

class BaseSensor
{
public:
    virtual void initialize()
    {
        std::cout << "Initializing base Sensor\n";
    }
};

class Sensor : public BaseSensor
{
public:
    void initialize() override
    {
        std::cout << "Initializing sensor\n";
    }
};

void boot(BaseSensor& dev)
{
    // Will call Sensor::initialize() if a Sensor is passed
    dev.initialize();
}

int main()
{
    //Obj of Sensor class
    Sensor s;

    boot(s);
}

Output:

Initializing sensor

 

In the above code, BaseSensor is a base class that defines a virtual function called initialize(). The derived class Sensor overrides this function to provide its own specific behavior.

Because initialize() is declared as virtual, C++ enables runtime polymorphism. This means that when you call initialize() using a base class reference or pointer, the program will automatically invoke the correct version based on the actual type of the object not just the type of the reference.

 

Why do we need virtual functions in C++?

We use virtual functions in C++ to enable runtime polymorphism. It is a key feature of object-oriented programming. This allows us to write code that is flexible, modular, and more extensible. It is ideal for large systems, evolving architectures, and hardware abstraction.

We use virtual functions in C++ to enable runtime polymorphism, a core concept in object-oriented programming. This feature allows us to write code that is flexible, modular, and easily extensible. it is ideal for large systems, evolving architectures, and hardware abstraction.

Here is a breakdown of why virtual functions are needed, especially with practical context:

1. To Enable Runtime Polymorphism:

 

Without virtual: Early Binding

Without the virtual keyword, C++ uses early binding (also known as compile-time binding). This means the compiler determines which function to call based on the type of the pointer or reference not the actual object it points to. As a result, if a derived class overrides a function but you call it through a base class reference or pointer, the base class version will be used.

#include<iostream>

class BaseSensor
{
public:
    void initialize()
    {
        std::cout << "Initializing base Sensor\n";
    }
};

class Sensor : public BaseSensor
{
public:
    void initialize()
    {
        std::cout << "Initializing sensor\n";
    }
};

int main()
{
    //Obj of Sensor class
    Sensor s;

    BaseSensor& rDev = s;

    rDev.initialize(); //Call as per reference
}

Output:

Initializing base Sensor

 

In the above code, Sensor class inherits from BaseSensor class. Both BaseSensor and Sensor define a function called initialize().
However, initialize() is not marked virtual in the base class. As a result, C++ uses static (compile-time) binding. This means the version of initialize() that gets called depends on the type of the reference, not the actual type of the object. So you can see in the above code output is “Initializing base Sensor”.

 

With virtual: Late Binding

You can solve this problem using virtual functions. In this case, C++ uses runtime (or late) binding. This means the function call is determined based on the actual type of the object, not just the type of the pointer or reference. This allows the overridden behavior in derived classes to be used—even when accessed through a base class pointer or reference.

#include<iostream>

class BaseSensor
{
public:
    virtual void initialize()
    {
        std::cout << "Initializing base Sensor\n";
    }
};

class Sensor : public BaseSensor
{
public:
    void initialize() override
    {
        std::cout << "Initializing sensor\n";
    }
};

int main()
{
    //Obj of Sensor class
    Sensor s;

    BaseSensor& rDev = s;

    rDev.initialize(); //Call as per the object
}

Output:

Initializing Sensor

This time, the initialize() method is resolved at runtime, based on the actual object type (Derived). Hence, Derived::initialize() is called.

 

⏰ Early Binding vs Late Binding:

Before understanding the difference between the early and late binding. It is good to understand the concet behind these two words.

Early Binding:

Early binding means the function call is decided at compile time based on the type of the pointer or reference. This is how regular (non-virtual) functions work.

Late Binding:

Late binding happens when you use the virtual keyword. With late binding, the function call is decided at runtime based on the actual type of the object. This allows derived classes to override functions and have their version called, even when using a base class pointer or reference. Late binding enables runtime polymorphism, which makes your code more flexible and reusable.

Now let’s understand the difference between these two concepts.

Feature Without virtual (Early Binding) With virtual (Late Binding)
Function Resolution At compile time At run time
Decided By Type of pointer or reference Type of the actual object (constructed)
Use Case Performance-critical, fixed behavior Flexible, polymorphic behavior

 

 

2. To Support Dynamic Behavior in Inheritance Hierarchies:

Virtual functions allow derived classes to customize or override base class behavior without changing the interface. Let’s take a closer look at how powerful the virtual keyword is, and how it can help you design cleaner, more reusable, and extensible code. Imagine this real-world embedded use case:

In the code below, I have written a generic communication logic using the ICommunication interface. The actual behavior of the send() method is determined at runtime based on whether you are using UART, SPI, or any other protocol you might add in the future. This makes the design flexible, reusable, and extensible.

 

class ICommunication
{
public:
    virtual void send(const std::string& data) = 0;
};

class UART : public ICommunication
{
public:
    void send(const std::string& data) override
    {
        // UART-specific logic
    }
};

class SPI : public ICommunication
{
public:
    void send(const std::string& data) override
    {
        // SPI-specific logic
    }
};

Now, your high-level code doesn’t care how data is transmitted.

void transmitData(ICommunication& rCom)
{
    rCom.send("Hello Aticleworld");
}

Whether it is UART, SPI, or future interfaces like CAN or BLE, transmitData() just works. That means the caller (transmitData()) doesn’t need to know the exact type of the object behavior is determined at runtime. This save a lot of time when we are changing MCU in our project.

 

3. Interface-Based Design

You can achieve loose coupling by designing your code around interfaces using pure virtual functions. As shown in the example above, the transmit logic (transmitData()) doesn’t need to know how the communication happens—whether it’s over UART, SPI, or any future protocol. It simply relies on the interface, making the code more modular, testable, and extensible.

 

4. Avoid Code Duplication:

Using the virtual function you can avoid the code duplication. It helps us to build reusable base classes.

 

❌ Common Pitfalls to Avoid When Using Virtual Functions:

Now that you’ve seen how powerful virtual functions can be in C++, it’s important to understand that with this power comes responsibility. While virtual functions enable dynamic behavior and flexible design, misusing them can lead to subtle bugs, poor architecture, or unnecessary complexity in your code.

Here are some common pitfalls to watch out for when working with virtual functions:

1. Forgetting to Use the override Keyword:

Not using override keyword can lead to silent errors if the function signature does not exactly match the base class.

// Risky

// Doesn't override due to missing '&'
void send(std::string data) { }  



// Safer

// Correct override
void send(const std::string& data) override { }

So, always use override to make your intentions clear and let the compiler catch mistakes.

 

2. Creating Pure Virtual Destructors Without a Body

If you declare a pure virtual destructor, you must still provide a definition. Failing to do so will result in a linker error.

class Interface
{
public:
    virtual ~Interface() = 0;  // OK to declare
};

Interface::~Interface() {}     // Must define it!

 

3. Not Making the Base Class Destructor Virtual

If you intend to delete derived objects through a base class pointer, always declare the base class destructor as virtual. Otherwise, only the base part will be destroyed, causing memory leaks. See the below example,

class Base 
{
public:
    // Always use virtual if class is meant to be inherited
    virtual ~Base() {}  
};

 

4. Object Slicing

Object slicing happens when a derived class object is passed by value to a base class variable or function. The derived part gets “sliced off,” and only the base part is kept. This disables polymorphism and can lead to unexpected behavior.

Example,

#include <iostream>

class Base
{
public:
    virtual void print()
    {
        std::cout << "Base class\n";
    }
};

class Derived : public Base
{
public:
    void print() override
    {
        std::cout << "Derived class\n";
    }
};

void printInfo(Base obj)    // Passed by value — slicing occurs!
{
    obj.print();
}

int main()
{
    Derived obj;

    printInfo(obj);  // Output: Base class
}

Output:

Base class

 

In the above example, Derived obj is passed by value to printInfo(Base obj). This causes the Derived part to be sliced off, so only the Base part remains. As a result, Base::print() is called, not Derived::print().

 

5. Overusing Virtual Functions

Not every function needs to be virtual. Use them only when polymorphic behavior is required. Unnecessary use of virtual function can increase complexity and runtime cost.

 

6. Don’t Call Virtual Functions in Constructors/Destructors:

At that point, the actual derived object is not fully constructed or is already partially destroyed. Consider the below example,

class Base
{
public:
    Base()
    {
        // BAD: foo() won’t behave polymorphically
        foo();    
    }
    virtual void foo() {}
};