Virtual Functions Under the Hood in C++

Virtual functions in C++ are a key feature that helps with code reusability and contributes to good software design. They are one of the pillars of polymorphism in the language. Polymorphism is the idea in which a program can use the same interfaces for many different underlying forms. Many people have confusions about how to use virtual functions when getting started with C++, let alone how they work under the hood. Even the name “virtual function” sounds a bit ominous but the implementation is actually fairly straight forward. So how are virtual functions used and how do they work under the hood?

 

What are Virtual Functions? 

Basics:

As stated above, virtual functions are one of the main implementations of Polymorphism in C++. An example of Polymorphism is a high level serial interface parent class. It should have many standard functions baked in such as Read(), Write() and Flush() but shouldn’t care if the underlying child serial class is a UART, SPI or any other serial interface. Virtual functions are member functions within a base class that can be re-defined or “overriden” in a child class.

Virtual functions are especially important for cases where a pointer of type “parent class” points to a child object. If the parent and child class both define the same member function, the base class implementation will be used which is often not desired.

class parent
{
public:
    void Print()
    {
        std::cout << "I am parent";
    }
};

class child : public parent
{
public:
    void Print()
    {
        std::cout << "I am child";
    }
};

int main()
{
    child childObject;
    parent* parentObject = &childObject;

    parentObject->Print();  // Prints "I am parent"
    return 0;
}

In most of these situations, the child class member function is the intended call and the use of virtual functions ensures this. The above case is often implemented to make code more readable and easier to modify. Using the serial interface example, you can imagine a class accepting a high level serial interface object pointer but passing in a specific child UART object. This way, if the specific interface type has to change to SPI, the accepting class does not need to be modified and will still call the correct child specific functions. Virtual functions are used to ensure the child class member function is called as shown below.

class parent 
{ 
public: 
    virtual void Print() 
    { 
        std::cout << "I am parent";
    } 
}; 

class child : public parent 
{ 
public: 
    void Print() override
    { 
        std::cout << "I am child" 
    } 
}; 

int main() 
{ 
    child childObject; 
    parent* parentObject = &childObject;
    parentObject->Print();    // Prints "I am child" 
    return 0; 
}

The override identifier signals to the compiler that this function is intended to override the parent class virtual function. This helps the compiler catch errors like incorrect return or parameter types that would otherwise be missed. This feature is only available in C++ 11 and newer.

Pure Virtual Functions:

Pure virtual functions are just a special case of virtual functions. A pure virtual function is a class member function that must be defined in the child class that inherits from it. The parent class that contains the pure virtual function is considered an abstract class which means no objects of the parent type can be instantiated. Any child class that inherits from the abstract parent class must define the pure virtual function or it will also become an abstract class. In the Serial Interface example, it would make sense to make the top level parent class an abstract class with pure virtual functions defined for various functionality like Read() and Write(). This is because the Read() and Write() functionality will completely depend on what type of serial communication (UART, SPI, I2C, etc…) is inheriting from the high level parent class. Pure virtual functions are denoted by setting the functions definition equal to zero as shown below. The child class re-defines the pure virtual functions exactly the same way as a regular virtual function shown above.

class parentSerialInterface
{
public:
    virtual char* Read() = 0;
    virtual void Write(char* dataOut) = 0;
};

 

How do Virtual Functions Actually Work?

Now that the use of virtual functions is clear, lets pull back the curtain that is the C++ compiler and examine how virtual functions are implemented under the hood. For regular functions, the C++ compiler performs what is called “early binding”. This means the compiler binds each function call to the intended function definition at compile time. While normal functions rely on early binding, virtual functions work using “runtime binding” meaning the actual function call address will be worked out at runtime. This is done using something called vtables and vpoiners.

Vtables:

Anytime a class implements a virtual function or is derived from a class which contains virtual functions, the compiler automatically adds what is called a vtable as a member of the class. The vtable is basically an array of function pointers which point to the most local virtual function within the class. Every virtual function within the class has a single entry in the vtable. Building from the previous example, here is a simple illustration to help explain this concept. The compiler automatically adds a hidden, public vtable pointer member to the most base class called a vpointer. We will add it explicitly below for clarity.

class parent
{ 
public : 
    vTable* __vptr;
    virtual void Print() 
    { 
        std::cout << "I am parent"; 
    } 
    virtual void Func1()
    {
        std::cout << "Parent function 1";
    }
}; 

class child1 : public parent 
{ 
public: 
    void Print() override 
    { 
        std::cout << "I am child1" 
    } 
};

class child2 : public parent
{
public :
    void Func1() override 
    {
        std::cout << "child2 Funct1";
    }

int main() 
{ 
    parent parentObject;
    child1 child1Object; 
    child2 child2Object;
}

The vpointer for each child class get’s inherited from the base class and points to the the most local vtable. This is how each object accesses the vtable of the associated class. Below is a diagram of the vtable layout for the above example. Notice that every virtual function defined in the base parent class has exactly one entry in each vtable. Also note that there is one vtable for each class no matter how many objects are created of that class type.

Graphic showing how virtual tables work with parent and child classes

Vpointers:

As stated above, vpointers are what allows an object to call the correct virtual function re-definition within the vtable. Where as each class, parent or child, has their own vtable, the compiler automatically adds a vpointer member to the base class only. When an object is created, the vpointer is inherited from the base class and set to point to the vtable of the specific class. For example, when a parent object is constructed, the vpointer is set to point to the parent class vtable and when a child object is constructed, the vpointer is set to point to the child class vtable.

Let’s re-explore the popular use of virtual functions where a parent object pointer is set to point to a child object and see how the vpointer facilitates the correct virtual function call.

int main() 
{ 
    child1 child1Object; 
    parent* basePtrObject = &childObject;

    basePtrObject->Print();    // Prints "I am child1"
}

When child1Object is constructed, the parent vpointer is set to point to the child1 class’s vtable for that instance. Then since basePtrObject points to the child object, the virtual function call to Print will reference the correct vtable and call the child1 function instead of the parent.

As you can see, the behind the scenes implementation of virtual functions is actually pretty straightforward but requires some “compiler magic”. This added complexity makes some nervous about using C++ and this type of inheritance in embedded environments.

Virtual Functions in Embedded Environments

Some developers avoid C++ and virtual functions in embedded code at all costs, touting that it leads to code bloat and performance issues. In 98% of cases, there are no issues using virtual functions for an embedded project as they are not as slow as people think and microcontrollers have so much processing power today. Rud Merriam has a fantastic article on hackaday.com about this exact topic which explores some test scenarios using straight C function calls and C++ virtual function calls.

He created a very simple test function which turns on an analog or digital pin. One implementation uses  straight C function calls and the other uses C++ virtual functions / inheritance. He then ran two test cases, one to turn on a single pin and the other to loop through an array and turn on every pin of the device. In the first case the function is only called once and the C implementation was faster by .2ms – .7ms depending on the microcontroller. But, in the second case the C++ implementation was actually faster. This was due to the extra code needed in the C version to determine wether a pin is analog or digital before calling the correct function. This is the main gotcha with C vs C++, C implementations require more instructions outside of the function calls to achieve the intended behavior. C++ abstracts more of the behavior into the class side, adding complexity inside the class and not main. This makes most comparisons a wash.

Going Further

Virtual functions are just one of the many powerful tools available in modern languages like C++. Take your programming skills to the next level with some advanced design patters and check out my post on Virtual Template Functions in C++!

Virtual Template Functions in C++

 

If you found this content interesting or useful, buy me a coffee!

Bitcoin Wallet: 

bc1q0vkx34w5y4yt5nq38a4rvk7gvgnxm2xv5lvyft

Related Articles

1 thought on “Virtual Functions Under the Hood in C++”

Leave a Comment

Your email address will not be published. Required fields are marked *

Braden Sunwold

Embedded Firmware Engineer

I love working on the boarder between software and hardware and creating devices that interact with the physical world.

Explore
Donate Now
Love BNE?

Donations like your's keep killer content coming