C + + object model: Memory layout for multiple inheritance and virtual inheritance

Source: Internet
Author: User
Tags inheritance

This is a translation, the original address: here; translated articles from: Click to open the link


This article mainly explains the virtual inheritance of C + + object memory distribution problem, which also leads to dynamic_cast and static_cast essential difference, virtual function table format, and some of the most C + + programmers are specious concepts. See here (by Edsko de Vries, January 2006)

Warning: This article is the introduction of C + + technical articles, assuming that readers have a more in-depth understanding of C + +, but also need some assembly knowledge.

In this article we will illustrate the object memory layout for the GCC compiler for multiple inheritance and virtual inheritance. Although in an ideal use environment, a C + + programmer does not need to understand the internal implementation details of these compilers, in fact, the various implementation details of the compiler for multiple inheritance (especially virtual inheritance) have more or less implications for our C + + code (e.g. downcasting pointer , pointers to pointers, and invocation order of virtual base class constructors. If you can understand how multiple inheritance is implemented, then you can foresee these effects yourself and then be able to deal with them well in your code. Moreover, if you are very concerned about the efficiency of the Code, it is also helpful to correctly understand the virtual inheritance. Finally, this hack process is very interesting oh:

Multiple Inheritance

First, let's consider a very simple (non-virtual) multiple inheritance. Take a look at the C + + class hierarchy below.

1 class top
2 {
3 Public:
4 int A;
5};
6
7 class left: public Top
8 {
9 Public:
Ten int b;
11};
12
class right: Public Top
14 {
Public :
int C;
17};
18
class Bottom: Public left, public right
20 {
Public :
int D;
23};
24
expressed in UML as follows:

Note that the top class is actually inherited two times (this mechanism is called repeated inheritance in Eiffel), which means that there are actually two a properties in a bottom object (attributes, which can go through bottom.) Left::a and bottom. Right::a access).

So left, right, bottom in the memory how to distribute it. Let's take a look at the simple left and right memory distributions:

[The layout of the right class is the same as left, so I'm not drawing anymore.] Hedgehog

Note that the first attribute of the class above is inherited from the top class, which means the following two assignment statements:

1 left* left =new left ();
2 top* top = left;

The left and top actually point to two identical addresses, and we can use the right object as a top object (as well as a top object). But what about the Botom object? This is how GCC handles:

But now what happens if we upcast a bottom pointer?

1 bottom* Bottom =new Bottom ();
2 left* left = bottom;

This piece of code is running correctly. This is because the memory layout chosen by GCC allows us to treat bottom objects as left objects, which are exactly the same. But what if we upcast the bottom object pointer to the right object?

1 right* right = bottom;

If we're going to make this code work, we need to adjust the pointer to the appropriate part of the bottom. This line of code can be executed because, relative to the derived class bottom, the right pointer to the base class points to the pointer bottom of the derived class object, This completes a pointer to a derived class object to a base class child object, which can exist as a stand-alone object or as part of a derived class object. 】。

By adjusting, we can use the right pointer to access the bottom object, when the bottom object behaves as if it were the object. But the bottom and right pointers point to different memory addresses. Finally, we consider the following:

1 top* top = bottom;

Well, there is no result, This statement is in fact ambiguous (ambiguous) , the compiler will be an error: Error: "Top" is a ambiguous base of ' Bottom '. In fact, these two ambiguous possibilities can be distinguished by the following statement:

1 top* topl = (left*) bottom;
2 top* topr = (right*) bottom;

After these two assignment statements are executed, theTOPL and left pointers will point to the same address, and TOPR and right will also point to the same address .

Virtual Inheritance

To avoid the above multiple inheritance of the top class of the base class, we must virtual inherit class top.

1 class Top
2 {
3 Public:
4 int A;
5};
6
7 class Left:virtual Public top
8 {
9 Public:
Ten int b;
11};
12
Class Right:virtual Public Top
14 {
Public :
int C;
17};
18
Class Bottom:public left.
20 {
Public :
int D;
23};
24

The above code will produce a class hierarchy diagram (in fact, this may be exactly the way you want to inherit the inheritance).

This class-level diagram is much simpler and clearer for programmers, but it's a lot more complicated for a compiler. We use the bottom memory layout as an example, it may be "the actual memory layout is not like this, specifically see behind" Is this:

The advantage of this memory layout is that its beginning (left) and left layouts are exactly the same, and we can easily access a bottom object through a left pointer. But let's think about right:

1 right* right = bottom;

What address should we assign to the right pointer? Theoretically, with this assignment, we can use this right pointer as a pointer to a proper object (now pointing to bottom).

But in reality it is unrealistic. A real right object memory layout and bottom object is completely different, so in fact we can not use this upcasted bottom object as a real right object. Moreover, there is no room for improvement in the design of our layout.

Here we'll look at how the memory is actually distributed , and then explain why it's so designed.

There are two points in the above picture that should be noted. The 1th is that the order in which members are distributed in a class is completely different (actually, quite the opposite). 2nd, the class adds a vptr pointer , which is inserted into the class by the compiler during compilation (if virtual inheritance is used when the class is designed, the virtual function produces the associated vptr). At the same time, the related pointers are initialized in the constructor of the class, which is also the work done by the compiler. The vptr pointer points to a "virtual table". each virtual base class in a class will have a vptr pointer corresponding to it. In order to show you the virtual table function, consider the following code.

1 bottom* Bottom = new Bottom ();
2 left* left = bottom;
3 int p = left->a;
The second assignment statement makes the left pointer point to the same starting address as bottom (that is, it points to the "top" of the bottom object). Let's consider the assignment statement for the third article.

1 movl  left,%eax        #%eax =left
2 movl    (%EAX),%eax      #%eax =left . vptr. Left
3 movl   (%eax),%eax      #%eax =virtual base  OFFSET&NBSP
4 addl  left,%eax        #%eax = left + Virtual base offset
5 movl   (%eax),%eax      #% EAX =LEFT.A
6 movl  %eax,p           # p  =LEFT.A

In summary, we use the left pointer to index (FIND) virtual table, and then we get the offset (virtual base offset, vbase) of the virtual base class in virtual table, and then add this offset to the left pointer. So we get the start address of the top class in the bottom class. from the above illustration, we can see that for the left pointer, its virtual base offset is 20, and if we assume that each member in the bottom is a 4-byte size, then the left pointer plus 20 bytes is exactly the address of member A.

We can also access the right part of the bottom in the same way.

1 bottom* Bottom = new Bottom ();
2 right* right = bottom;
3 int p = right->a;

The right pointer points to the appropriate location in the bottom object.

The assignment statement for P will eventually be compiled into the same way as the left above to access a. The only difference is that vptr, the vptr we're visiting now points to another address on virtual table , and the virtual base offset becomes 12. We draw a summary of:

The key point, of course, is that we want to be able to access a truly separate right object as well as access to a bottom object that passes through the upcasted (to the right object). Here we also introduce vptrs to the right object.

OK, now this design finally allows us to access the bottom object through a right pointer. However, it is important to note that the above design entails a considerable cost : We need to introduce a virtual function table, and the underlying object must also be extended to support one or more virtual function pointers, and a simple member access now needs to be addressed two times by virtual function table ( Compiler optimizations can mitigate performance losses to some extent.

downcasting

As we suspect, converting a pointer from a derived class to a base class (casting) involves adding an offset to the pointer. A friend might guess, downcasting a pointer just minus some offsets. In fact, this is true in the case of non-virtual inheritance, but for virtual inheritance, other complex problems have to be introduced. Here we add some inheritance relationships in the example above:

1 class Anotherbottom:public left.
2 {
3 Public:
4 int e;
5 int F;
6};

This inheritance relationship is shown in the following illustration:

So now consider the following code

1 bottom* bottom1 =new Bottom ();
2 anotherbottom* bottom2 = new anotherbottom ();
3 top* top1 = bottom1;
4 top* top2 = bottom2;
5 left* left = static_cast<Left*> (TOP1);
The following diagram shows the memory layout of bottom and anotherbottom, and also shows where each top pointer points.

Now let's consider the static_cast from Top1 to left, and note that it is not clear to us whether the object to which the TOP1 pointer refers is bottom or anotherbottom. This is simply not possible to compile through. Because it is not possible to confirm the offset at which the TOP1 runtime needs to be adjusted (for bottom is 20, for Anotherbottom is 24). so the compiler will raise the error: Error:cannot convert from base ' up ' to derived type ' left ' via virtual base ' top '. Here we need to know the run-time information, so we need to use dynamic_cast:

1 left* left =dynamic_cast<Left*> (TOP1);

However, the compiler still complains about Error:cannot dynamic_cast ' top ' (of type ' class top* ') to type ' class left* ' (Source type isn't polymorphic )。 The key problem is that using dynamic_cast (as with typeid) needs to know the Run-time information of the object that the pointer refers to. but looking back at the chart above, we'll find that the TOP1 pointer simply refers to an integer member a (the top class's pointer top1 or TOP2 points to the part of a derived class object pointer bottom1 or the upper class object in the Bottom2, where it points to: A, and does not point to the part of the derived class of top. The compiler does not include a vptr for top in the bottom class, which it considers absolutely unnecessary. to force the compiler to include top vptr in the bottom, we can add a virtual destructor inside the top class of the Polymorphic base class (reference article 07 declares the virtual destructor for the polymorphic base class ).

1 class top
2 {
3 Public:
4 Virtual ~top () {}
5 int A;
6};

This forces the compiler to add a vptr to the top class. Here's a look at bottom's new memory layout:

Yes, other derived classes (left and right) will add a vptr.top, and the compiler generates a library function call for dynamic_cast.

1 left = __dynamic_cast (Top1, Typeinfo_for_top, Typeinfo_for_left,-1);

__dynamic_cast is defined in libstdc++ (the corresponding header file is cxxabi.h), and the conversion is performed with the type information of top, left, and bottom. Where parameter-1 represents the relationship between the class left and the top of class is not clear. For more information, see the implementation of tinfo.cc.

Summary

Finally, we'll talk about some of the relevant content.

Level Two pointer

The problem here is a bit confusing at first, but it is obvious that there are some problems. Here we consider a question, or the class inheritance structure diagram in the downcasting of the above section as an example.

1 bottom* b =new Bottom ();
2 right* r = B;

(When you assign the value of a B pointer to pointer r, the b pointer adds 8 bytes, so that the r pointer points to the right part of the bottom object). So we can assign the value of the bottom* type to the Right* object. But bottom** and right** two types of pointers are assigned to each other.

1 bottom** bb = &b;
2 right** rr = BB;

Can the compiler pass these two statements? The compiler will actually make an error: Error:invalid conversion from ' bottom** ' to ' right** '
Why? Consider, conversely, that if you can assign a BB to a RR, the following illustration shows. So here BB and RR two pointers all point to B,b and R all point to the corresponding part of the bottom object. Now consider what will happen if you assign a value to *RR.

1 *RR = b;

Note that the *RR is a pointer to the right* type (level one), so the assignment is valid.

This is the same as the one we gave the R pointer to (*RR is the first-level right* type pointer, and R is also a right* pointer). Therefore, the compiler will implement the *RR assignment operation in the same way. In fact, we have to adjust the value of B, plus 8 bytes, and then assign the value to *RR, but now **RR actually point to B! The following figure

Well, if we were to access the bottom object through a RR, we would be able to get access to the bottom object according to the graph above, but if we were to access the bottom object with B, all the object references were actually 8 bytes--obviously wrong.

All in all, although the *a and *b can rely on class inheritance relationships to transform each other, **a and **b cannot have this inference .

constructors for virtual base classes

The compiler must ensure that all virtual function pointers are properly initialized. in particular, the constructors for all virtual base classes in the class are called and can only be invoked once. If you write your code without displaying the call constructor, the compiler automatically inserts a section of the constructor call code. This will result in some strange results, as well as consider the class inheritance diagram above, but join the constructor.

1classTop
2 {
3 Public:
4 Top () {a =-1;}
5 Top (int_a) {a = _a;}
6intA
7};
8
9classLeft: PublicTop
10 {
11 Public:
Left () {b =-2;}
Left (int_a,int_b): Top (_a) {b = _b;}
14intb
15};
16
17classRight: PublicTop
18 {
19 Public:
Right () {c =-3;}
Right (int_a,int_c): Top (_a) {c = _c;}
22intC
23};
24
25classBottom: PublicLeft, PublicRight
26 {
27 Public:
Bottom () {d =-4;}
Bottom (int_a,int_b,int_c,int_d): Left (_a, _b), right (_a, _c)
30 {
D = _d;
32}
33intD
34};
35
First consider the case where the virtual function is not included, what does the following code output?

1 Bottom Bottom (1,2,3,4);
2 printf ("%d%d%d%d/n", bottom). Left::a, bottom. Right::a, Bottom.b, BOTTOM.C, BOTTOM.D);
You might have guessed that this would be the result:

1 1 2 3 4

However, if we consider the case containing the virtual function, if we derive the subclass from top virtual inheritance, we will get the following result:

-1-1 2 3 4
As mentioned at the beginning of this section, the compiler inserts a default constructor for top in bottom, and the default constructor is arranged before the other constructors, and when left begins to call its base class constructor, we find that the top is already constructed initialized, so the corresponding constructor is not invoked. If you trace the constructor, we will see

Top::top ()
Left::left (1,2)
Right::right (1,3)
Bottom::bottom (1,2,3,4)
To avoid this situation, we should show the constructor of the virtual base class to be called

1 Bottom ( int  _a, int  _b, int  _c,

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.