When virtual Functions Won’t Fall inline

C++ offers two useful modifiers for class methods: virtual and inline. A virtual method can inherit new behavior from derived classes, while the inline modifier requests that the compiler place the content of the method “inline” wherever the method is invoked rather than having a single instance of the code that is called from multiple places. You can think of it as simply having the compiler expand the content of your method wherever you invoke the method. But how does the compiler handle these modifiers when they are used together?

First, a sample class.

class Number
{
protected:
    int mNumber;

public:
    Number(int inNumber)
        : mNumber(inNumber)
    {
    }

    inline int getNumberInline() const
    {
        return mNumber;
    }

    virtual inline int getNumber() const
    {
        return mNumber;
    }
};

We can create an evaluation function to exercise our class:

void evalNumber(const Number* num)
{
    int val = num->getNumber();
    int inl = num->getNumberInline();
    printf("val = %d, inl = %d", val, inl);
}

And we can call our evaluation function, providing an instance of the Number class:

    Number* num = new Number(5);
    evalNumber(num);
    delete num;

The disassembly of evalNumber looks like this (using Microsoft Visual C++ 2008):

void evalNumber(const Number* num)
{
00CE2010  push        ebp  
00CE2011  mov         ebp,esp 
00CE2013  sub         esp,8 
    int val = num->getNumber();
00CE2016  mov         eax,dword ptr [num] 
00CE2019  mov         edx,dword ptr [eax] 
00CE201B  mov         ecx,dword ptr [num] 
00CE201E  mov         eax,dword ptr [edx] 
00CE2020  call        eax  
00CE2022  mov         dword ptr [val],eax 
    int inl = num->getNumberInline();
00CE2025  mov         ecx,dword ptr [num] 
00CE2028  mov         edx,dword ptr [ecx+4] 
00CE202B  mov         dword ptr [inl],edx 
    printf("val = %d, inl = %d", val, inl);
00CE202E  mov         eax,dword ptr [inl] 
00CE2031  push        eax  
00CE2032  mov         ecx,dword ptr [val] 
00CE2035  push        ecx  
00CE2036  push        offset __load_config_used+48h (0CE49D0h) 
00CE203B  call        dword ptr [__imp__printf (0CE724Ch)] 
00CE2041  add         esp,0Ch 
}
00CE2044  mov         esp,ebp 
00CE2046  pop         ebp  
00CE2047  ret              

You’ll notice that when invoking the virtual inline method the compiler inserts a call to the method’s implementation while the inline version of our function is expanded in-place. So why didn’t our virtual inline method get expanded in-place as well?

The reason lies in how virtual methods work. When a C++ compiler encounters a virtual method, it typically creates a virtual method table (or v-table) for the class, containing pointers to each virtual method for the class. When an instance of the class is created, it contains a pointer to the v-table. Invoking the virtual method requires a look-up in the v-table to retrieve the address for the correct implementation of the method. Instances of derived classes are simply able to point to a different v-table to override behavior in a base class. Understanding how v-tables work, it should be apparent why the compiler couldn’t expand the virtual inline method in place. It’s possible num pointed to an object derived from Number and the implementation of getNumber() had been overridden. In this case, the compiler had to go through the v-table to ensure it invoked the correct method implementation.

So does virtual inline buy us anything? As it turns out, the compiler can take advantage of the inline declaration when it can determine with certainty the type of the object being referenced.

    Number num2(5);
    int dblVal = num2.getNumber();
    printf("dblVal = %d", dblVal);

When we reference num2 as a local variable, the compiler can determine from the context that we are referencing an instance of the Number class and not another class derived from Number. This allows the compiler to generate the following code:

    Number num2(5);
009220B5  mov         dword ptr [num2],offset NumberDoubler::`vftable' (924798h) 
009220BC  mov         dword ptr [ebp-8],5 
    int dblVal = num2.getNumber();
009220C3  mov         edx,dword ptr [ebp-8] 
009220C6  mov         dword ptr [dblVal],edx 
    printf("dblVal = %d", dblVal);
009220C9  mov         eax,dword ptr [dblVal] 
009220CC  push        eax  
009220CD  push        offset __load_config_used+5Ch (9249E4h) 
009220D2  call        dword ptr [__imp__printf (92724Ch)] 
009220D8  add         esp,8

You can see the code for getNumber() has been expanded in-place. It’s important to realize the compiler can only make this optimization because it knows the object’s type with certainty and, therefore, doesn’t need to go through the v-table to call the method. Instead the inline method can be expanded in-place.

Leave a Reply

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