Memory Layout for Multiple and Virtual Inheritance (一) (部分翻译)

Memory Layout for Multiple and Virtual Inheritance 

Sources: http://www.phpcompiler.org/articles/virtualinheritance.html

本文主要介绍了gcc编译器中multiple 和 virtual inheritance中的对象内存布局。虽然C++程序员不需要关心编译器的内部细节,但多重继承(特别是虚拟继承)在C++代码实现中有不同的结果(特别是用pointers to pointers 来实现downcasting pointers,constructors for virtual bases的调用顺序)。如果你了解多重继承和虚拟继承,你就能够更好的了解你代码,同时,你也能够理解虚拟继承带来的开销。

多重继承:

首先让我们来考虑(非虚拟化)多重继承的例子。

class Top

{

public:

int a;

};

class Left : public Top

{

public:

int b;

};

class Right : public Top

{

public:

int c;

};

class Bottom : public Left, public Right

{

public:

int d;

};

用UML图来表示,可以得到如下:

我们可以看到top被继承两次,也就是说,在类型Bottom中的对象bottom将有两个属性值为a(bottom.left::a,bottom.right::a)。

那么,Left,Right和Bottom在memory中如何布局呢?我们来看第一个case。Left和Right有下面的结构:


Left


Top::a


Left::b


Right


Top::a


Right::c

注意到第一个属性来自于Top。那么意味着下面的赋值:

Left* left = new Left();

Top* top = left;

Left和top指向一个相同的地址,我们可以认为是Left对象,如何它是一个Top对象(明显Right也是这样的)。对于Bottom呢?gcc中如下


Bottom


Left::Top::a


Left::b


Right::Top::a


Right::c


Bottom::d

那么,我们向上转换Bottom指针?

Bottom* bottom = new Bottom();

Left* left = bottom;

这个工作很好。那么对于转换成Right指针呢?

我们必须调整pointer来指向Right。

 
Bottom

 
Left::Top::a

 
Left::b


right


Right::Top::a

 
Right::c

 
Bottom::d

在调整之后,我们可以通过right指针访问到bottom。然后bottom和right指向不同的内存位置。那么从完整性角度,下面将会发生什么呢?

Top* top = bottom;

这条语句会产生歧义: error:Top is an ambiguous base of Bottom

可能产生两条语句,如下:

Top* topL = (Left*) bottom;
Top* topR = (Right*) bottom;

这两天语句之后,topL和left指向同一个地址,topR和right指向同一个地址;

Virtual Inheritance

To avoid the repeated inheritance of Top, we must inherit virtually from Top:

class Top
{
public:
   int a;
};

class Left : virtual public Top
{
public:
   int b;
};

class Right : virtual public Top
{
public:
   int c;
};

class Bottom : public Left, public Right
{
public:
   int d;
};

This yields the following hierarchy (which is perhaps what you expected in the first place)

while this may seem more obvious and simpler from a programmer‘s point of view, from the compiler‘s point of view, this is vastly more complicated. Consider the layout of Bottom again. One (non) possibility is

Bottom
Left::Top::a
Left::b
Right::c
Bottom::d

The advantage of this layout is that the first part of the layout collides with the layout of Left, and we can thus access a Bottom easily through a Left pointer. However, what are we going to do with

Right* right = bottom;

Which address do we assign to right? After this assignment, we should be able to use right as if it were pointing to a regular Right object. However, this is impossible! The memory layout of Right itself is completely different, and we can thus no longer access a “real” Right object in the same way as an upcasted Bottom object. Moreover, no other (simple) layout forBottom will work.

The solution is non-trivial. We will show the solution first and then explain it.

You should note two things in this diagram. First, the order of the fields is completely different (in fact, it is approximately the reverse). Second, there are these new vptr pointers. These attributes are automatically inserted by the compiler when necessary (when using virtual inheritance, or when using virtual functions). The compiler also inserts code into the constructor to initialise these pointers.

The vptrs (virtual pointers) index a “virtual table”. There is a vptr for every virtual base of the class. To see how the virtual table (vtable) is used, consider the following C++ code.

Bottom* bottom = new Bottom();
Left* left = bottom;
int p = left->a;

The second assignment makes left point to the same address as bottom (i.e., it points to the “top” of the Bottom object). We consider the compilation of the last assignment (slightly simplified):

movl  left, %eax        # %eax = left
movl  (%eax), %eax      # %eax = left.vptr.Left
movl  (%eax), %eax      # %eax = virtual base offset
addl  left, %eax        # %eax = left + virtual base offset
movl  (%eax), %eax      # %eax = left.a
movl  %eax, p           # p = left.a

In words, we use left to index the virtual table and obtain the “virtual base offset” (vbase). This offset is then added to left, which is then used to index the Top section of theBottom object. From the diagram, you can see that the virtual base offset for Left is 20; if you assume that all the fields in Bottom are 4 bytes, you will see that adding 20 bytes toleft will indeed point to the a field.

With this setup, we can access the Right part the same way. After

Bottom* bottom = new Bottom();
Right* right = bottom;
int p = right->a;

right will point to the appropriate part of the Bottom object:

  Bottom
  vptr.Left
  Left::b
right   vptr.Right
  Right::c
  Bottom::d
  Top::a

The assignment to p can now be compiled in the exact same way as we did previously for Left. The only difference is that the vptr we access now points to a different part of the virtual table: the virtual base offset we obtain is 12, which is correct (verify!). We can summarise this visually:

Of course, the point of the exercise was to be able to access real Right objects the same way as upcasted Bottom objects. So, we have to introduce vptrs in the layout of Right (and Left) too:

Now we can access a Bottom object through a Right pointer without further difficulty. However, this has come at rather large expense: we needed to introduce virtual tables, classes needed to be extended with one or more virtual pointers, and a simple attribute lookup in an object now needs two indirections through the virtual table (although compiler optimizations can reduce that cost somewhat).

Downcasting

As we have seen, casting a pointer of type DerivedClass to a pointer of type SuperClass (in other words, upcasting) may involve adding an offset to the pointer. One might be tempted to think that downcasting (going the other way) can then simply be implemented by subtracting the same offset. And indeed, this is the case for non-virtual inheritance. However, virtual inheritance (unsurprisingly!) introduces another complication.

Suppose we extend our inheritance hierarchy with the following class.

class AnotherBottom : public Left, public Right
{
public:
   int e;
   int f;
};

The hierarchy now looks like

Now consider the following code.

Bottom* bottom1 = new Bottom();
AnotherBottom* bottom2 = new AnotherBottom();
Top* top1 = bottom1;
Top* top2 = bottom2;
Left* left = static_cast<Left*>(top1);

The following diagram shows the layout of Bottom and AnotherBottom, and shows where top is pointing after the last assignment.

  Bottom
  vptr.Left
  Left::b
  vptr.Right
  Right::c
  Bottom::d
top1   Top::a
  AnotherBottom
  vptr.Left
  Left::b
  vptr.Right
  Right::c
  AnotherBottom::e
  AnotherBottom::f
top2   Top::a

Now consider how to implement the static cast from top1 to left, while taking into account that we do not know whether top1 is pointing to an object of type Bottom or an object of typeAnotherBottom. It can‘t be done! The necessary offset depends on the runtime type of top1 (20 for Bottom and 24 for AnotherBottom). The compiler will complain:

error: cannot convert from base `Top‘ to derived type `Left‘
via virtual base `Top‘

Since we need runtime information, we need to use a dynamic cast instead:

Left* left = dynamic_cast<Left*>(top1);

However, the compiler is still unhappy:

error: cannot dynamic_cast `top‘ (of type `class Top*‘) to type
   `class Left*‘ (source type is not polymorphic)

The problem is that a dynamic cast (as well as use of typeid) needs runtime type information about the object pointed to by top1. However, if you look at the diagram, you will see that all we have at the location pointed to by top1 is an integer (a). The compiler did not include a vptr.Top because it did not think that was necessary. To force the compiler to include this vptr, we can add a virtual destructor to Top:

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

This change necessitates a vptr for Top. The new layout for Bottom is

(Of course, the other classes get a similar new vptr.Top attribute). The compiler now inserts a library call for the dynamic cast:

left = __dynamic_cast(top1, typeinfo_for_Top, typeinfo_for_Left, -1);

This function __dynamic_cast is defined in libstdc++ (the corresponding header file is cxxabi.h); armed with the type information for TopLeft and Bottom (through vptr.Top), the cast can be executed. (The -1 parameter indicates that the relationship between Left and Top is presently unknown). For details, refer to the implementation in tinfo.cc.

Concluding Remarks

Finally, we tie a couple of loose ends.

(In)variance of Double Pointers

This is were it gets slightly confusing, although it is rather obvious when you give it some thought. We consider an example. Assume the class hierarchy presented in the last section (Downcasting). We have seen previously what the effect is of

Bottom* b = new Bottom();
Right* r = b;

(the value of b gets adjusted by 8 bytes before it is assigned to r, so that it points to the Right section of the Bottom object). Thus, we can legally assign a Bottom* to a Right*. What about Bottom** and Right**?

Bottom** bb = &b;
Right** rr = bb;

Should the compiler accept this? A quick test will show that the compiler will complain:

error: invalid conversion from `Bottom**‘ to `Right**‘

Why? Suppose the compiler would accept the assignment of bb to rr. We can visualise the result as:

So, bb and rr both point to b, and b and r point to the appropriate sections of the Bottom object. Now consider what happens when we assign to *rr (note that the type of *rr is Right*, so this assignment is valid):

*rr = b;

This is essentially the same assignment as the assignment to r above. Thus, the compiler will implement it the same way! In particular, it will adjust the value of b by 8 bytes before it assigns it to *rr. But *rr pointed to b! If we visualise the result again:

This is correct as long as we access the Bottom object through *rr, but as soon as we access it through b itself, all memory references will be off by 8 bytes — obviously a very undesirable situation.

So, in summary, even if *a and *b are related by some subtyping relation, **a and **b are not.

Constructors of Virtual Bases

The compiler must guarantee that all virtual pointers of an object are properly initialised. In particular, it guarantees that the constructor for all virtual bases of a class get invoked, and get invoked only once. If you don‘t explicitly call the constructors of your virtual superclasses (independent of how far up the tree they are), the compiler will automatically insert a call to their default constructors.

This can lead to some unexpected results. Consider the same class hierarchy again we have been considering so far, extended with constructors:

class Top
{
public:
   Top() { a = -1; }
   Top(int _a) { a = _a; }
   int a;
};

class Left : public Top
{
public:
   Left() { b = -2; }
   Left(int _a, int _b) : Top(_a) { b = _b; }
   int b;
};

class Right : public Top
{
public:
   Right() { c = -3; }
   Right(int _a, int _c) : Top(_a) { c = _c; }
   int c;
};

class Bottom : public Left, public Right
{
public:
   Bottom() { d = -4; }
   Bottom(int _a, int _b, int _c, int _d) : Left(_a, _b), Right(_a, _c)
	{
      d = _d;
	}
   int d;
};

(We consider the non-virtual case first.) What would you expect this to output:

Bottom bottom(1,2,3,4);
printf("%d %d %d %d %d\n", bottom.Left::a, bottom.Right::a,
   bottom.b, bottom.c, bottom.d);

You would probably expect (and get)

1 1 2 3 4

However, now consider the virtual case (where we inherit virtually from Top). If we make that single change, and run the program again, we instead get

-1 -1 2 3 4

Why? If you trace the execution of the constructors, you will find

Top::Top()
Left::Left(1,2)
Right::Right(1,3)
Bottom::Bottom(1,2,3,4)

As explained above, the compiler has inserted a call to the default constructor in Bottom, before the execution of the other constructors. Then when Left tries to call its superconstructor (Top), we find that Top has already been initialised and the constructor does not get invoked.

To avoid this situation, you should explicitly call the constructor of your virtual base(s):

Bottom(int _a, int _b, int _c, int _d): Top(_a), Left(_a,_b), Right(_a,_c)
{
   d = _d;
}

Pointer Equivalence

Once again assuming the same (virtual) class hierarchy, would you expect this to print “Equal”?

Bottom* b = new Bottom();
Right* r = b;

if(r == b)
   printf("Equal!\n");

Bear in mind that the two addresses are not actually equal (r is off by 8 bytes). However, that should be completely transparent to the user; so, the compiler actually subtracts the 8 bytes from r before comparing it to b; thus, the two addresses are considered equal.

Casting to void*

Finally, we consider what happens we can cast an object to void*. The compiler must guarantee that a pointer cast to void* points to the “top” of the object. Using the vtable, this is actually very easy to implement. You may have been wondering what the offset to top field is. It is the offset from the vptr to the top of the object. So, a cast to void* can be implemented using a single lookup in the vtable. Make sure to use a dynamic cast, however, thus:

dynamic_cast<void*>(b);
时间: 2024-10-17 20:27:07

Memory Layout for Multiple and Virtual Inheritance (一) (部分翻译)的相关文章

Memory Layout for Multiple and Virtual Inheritance

2016/6/7 phc ­­ Memory Layout for Multiple and Virtual Inheritance ­­ Edsko de Vrieshttp://www.phpcompiler.org/articles/virtualinheritance.html 1/10Home | Download phc | Documentation | Developers and Contributors | Mailing List |Memory Layout for Mu

C++ 多继承和虚继承的内存布局(Memory Layout for Multiple and Virtual Inheritance)

警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识. 在本文中,我们解释由gcc编译器实现多继承和虚继承的对象的布局.虽然在理想的C++程序中不需要知道这些编译器内部细节,但不幸的是多重继承(特别是虚拟继承)的实现方式有各种各样的不太明确的结论(尤其是,关于向下转型指针,使用指向指针的指针,还有虚拟基类的构造方法的调用命令). 如果你了解多重继承是如何实现的,你就能预见到这些结论并运用到你的代码中.而且,如果你关心性能,理解虚拟继承的开销也是非常有用的.最后,这很有趣. :-) 多重

Memory Layout (Virtual address space of a C process)

Memory Layout (Virtual address space of a C process) 分类: C语言基础2012-12-06 23:16 2174人阅读 评论(0) 收藏 举报 found a good example to demostrate the memory layout and its stack info of a user-mode process, only that this example is for Linux. But it is still wo

Memory Layout of C Programs

from apue 7.6. Memory Layout of a C Program A typical memory representation of C program consists of following sections. 1. Text segment2. Initialized data segment    2.1 initialized read-only area    2.2 initialized read-write area3. Uninitialized d

C++: virtual inheritance and Cross Delegation

Link1: Give an example Note: I think the Storable::Write method should also be pure virtual. http://www.cprogramming.com/tutorial/virtual_inheritance.html Link2: explained from the vritual table point of view, Key point: as virual inheritance, compli

PatentTips - Method to manage memory in a platform with virtual machines

BACKGROUND INFORMATION Various mechanisms exist for managing memory in a virtual machine environment. A virtual machine platform typically executes an underlying layer of software called a virtual machine monitor (VMM) which hosts one to many operati

C语言内存模型 (C memory layout)

 一. 内存模型                                                                         1. .text 代码区(code section).由编译器链接器生成的可执行指令,程序执行时由加载器(loader)从可执行文件拷贝到内存中.为了安全考虑,防止别的区域更改代码区数据(即可执行指令),代码区具有只读属性.另一个方面,代码区通常具有可共享性(sharable),即在内存中只有一份代码区,如编译器,假如同时有多个编译任务

System and method for parallel execution of memory transactions using multiple memory models, including SSO, TSO, PSO and RMO

A data processor supports the use of multiple memory models by computer programs. At a device external to a data processor, such as a memory controller, memory transactions requests are received from the data processor. Each memory transaction reques

C++ virtual inheritance ZZ

虚继承 是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类. 举例来说:假如类A和类B各自从类X派生(非虚继承且假设类X包含一些数据成员),且类C同时多继承自类A和B,那么C的对象就会拥有两套X的实例数据(可分别独立访问,一般要用适当的消歧义限定符).但是如果类A与B各自虚继承了类X,那么C的对象就只包含一套类X的实例数据.对于这一概念典型实现的编程语言是C++. 这一特性在多重继承应用中非常有用,可以使得虚基类对于由它直