2010年11月14日

Filled Under:

Inheritance, Virtual Method Table, and Memory Layout of Class

Share
Recently, I have encountered a quite obscure bug when implementing reference counting pattern. This pattern related no more than two classes and 100 lines C++ code. However, it took me whole day to find the root cause. To remind many other C++ programmers in the hell (and to practice my writing skill), I wrote  this article to complain the design of C++ explain how inheritance and virtual methods affect the memory layout of a class.

Let's start from the bug I encountered. Following is a simple program used to illustrate this bug.

class Base{
public:
int a;
int b;
};
class Derived : public Base{
public:
int c;
virtual void methodA(){
return;
}
};
int main(){
// create derived class
Derived* derived_ptr = new Derived();
// cast derived point to base pointer
Base* base_ptr = static_cast<base*>(derived_ptr);
// delete derived class with base pointer
delete base_ptr;
// exit
return 0;
}

If you run the binary compiled from VC++, run-time exception (i.e. crash) will be definitely raised when deleting the object of Drived. After trying to set up break points, runing code line by line, looking up C++ standard to make sure this is not the bug of compiler, you may be surprised that you just need a virtual destructor for Base.

class Base{
public:
// virtual destructor
virtual ~Base(){}
int a;
int b;
};

If you've already realized the root cause from the solution and the title, you need not to continue reading this article. For those who feel confusing, the following is the explanation with delicate illustrations.

Have a look at this diagram:


This is a diagram showing the object memory layout of class Derived. It includes a continuous sub-section (labelled as blue blocks) that holds the structure of class Base. You would notice there is compiler generated member __vptr, which is the pointer to the virtual method table in the head of the memory layout of class Derived. Class Base doesn't have __vptr because it doesn't have any virtual table. Therefore, if you cast a pointer from Derived* to class Base* actually result in a shift of 4 bytes (for 32 bits system). That's why the program crashes if you delete the pointer base_ptr. The deleting address is not the address where object created!

Let's see what's happing when the virtual destructor is added to class Base:

Because class Base now have virtual method (it's destructor), the object memory layout of class base must also have a space for __vptr. Therefore, when you access the object of class Derived with the pointer of class Base, the computer will recognize blue blocks as the memory block of the object. Therefore, pointer casting between class Derived and class Base results in no address shift.

This is not the end of the story. The object memory layout gets more complicated when it comes to multiple inheritance. Consider the following program:

class Base{
public:
virtual ~Base(){}
int a;
int b;
};

class Base2{
public:
virtual ~Base2(){}
int a2;
int b2;
};

class Derived : public Base, public Base2{
public:
int c;
virtual void methodA(){
return;
}
};
int main(){
// create derived object
Derived* derived_ptr = new Derived();
// cast Derived pointer to Base pointer
Base* base_ptr = static_cast<base*>(derived_ptr);
// cast Derived pointer to Base2 pointer
Base2* base2_ptr = static_cast<base2*>(derived_ptr);
// delete derived object with base2 pointer
delete base2_ptr;
return 0;
}

The object memory layout of class Derived becomes:
The pointer base2_ptr has 12 bytes shift in comparison with derived_ptr. You might think this program will crash when calling delete with base2_ptr due to address shifting. Unexpectedly, the derived object is correctly deleted, because class Base2 also has virtual destructor, and virtual function mechanism do help address the deletion, thus avoiding free memory in wrong address.

Conclusion

You must declare virtual destructor for a class that is expected to be inherited by other classes.

0 Comments:

張貼留言