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.