understanding vptr in multiple inheritance?
Asked Answered
E

5

20

I am trying to make sense of the statement in book effective c++. Following is the inheritance diagram for multiple inheritance.

enter image description here

enter image description here

Now the book says separate memory in each class is required for vptr. Also it makes following statement

An oddity in the above diagram is that there are only three vptrs even though four classes are involved. Implementations are free to generate four vptrs if they like, but three suffice (it turns out that B and D can share a vptr), and most implementations take advantage of this opportunity to reduce the compiler-generated overhead.

I could not see any reason why there is requirement of separate memory in each class for vptr. I had an understanding that vptr is inherited from base class whatever may be the inheritance type. If we assume that it shown resultant memory structure with inherited vptr how can they make the statement that

B and D can share a vptr

Can somebody please clarify a bit about vptr in multiple inheritance?

  • Do we need separate vptr in each class ?
  • Also if above is true why B and D can share vptr ?
Elishaelision answered 16/4, 2011 at 4:58 Comment(24)
Unless you are a compiler writer, this is not important or relevant to using the language. In fact there is nothing in the standard about vptr or how they are used (or how to use them). This is because this is an implementation detail (the compiler may not even use vptr). So how this is done is very specific to each compiler and anything said here is only speculation (unless the answer comes from a compiler writer) and even then the answer will be specific to a particular version of a particular compiler.Redeeming
Having said all that. You only need one vptr if there is no virtual inheritance. Otherwise you need a vptr per class. Removing the last vptr for D is a specific optimization designed to reduce the size of the class. I found "Advanced Compiler Design and Implementation" very useful.Redeeming
Very interesting question. Haven't figured it out yet but I have the feeling the vptr for A and D is actually shared in the diagram as the data members of D are stored directly after those of A.Margalit
@Eelke: no, because of virtual inheritance A is handled very specially in such a case.Dextro
@LokiAstari "You only need one vptr if there is no virtual inheritance." Incorrect. With MI, the object gets multiple vptr.Verwoerd
@curiousguy: The key word here is "Need" (and I am correct (read the book I linked above)). You may find specific implementations that inject more, but this is because it is a compiler specific implementation detail. But this just emphasis my original point unless you are a compiler engineer and actually do this for a living there is no point in knowing this stuff (as in reality programmers only barely understand the basics of the how they are implementation anyway) and it is not relevant to the language usage.Redeeming
@LokiAstari Yes, with MI, the object must get multiple vptr in practical cases. I am not sure what you are trying to say, as you could use 0 vptr anyway.Verwoerd
@curiousguy: You really need to read the book before you make any more comments. Most programers (unless you are a compiler engineer) have only a basic understanding of what is actually required and your comments are correct at a superficial level but ignore all the real hard work and research done by compiler writers over the last ten years.Redeeming
@LokiAstari 1) Any absolute statement about "needed" vptr is meaningless; no vptr is ever "needed". Polymorphism can be implemented without a vptr. We assume polymorphism is implemented with a vptr, because it is the only general and efficient implementation (in very special cases, you could use other means, but in general, you rarely beat the efficiency of vptr in term of speed or space). 2) But if we assume that virtual calls are implemented in term of vtable, directly designated by a vptr, we can make statements about "needed" vptr. There are many implicit assumptions made here.Verwoerd
(...) We also assume separate compilation. Some transformations could be done if the compiler could need all classes definitions at once, and we usually assume that the compiler is not allowed to require that. So, we assume that the layout of a class does not depend its derived classes. Other approaches are possible. Global optimisation is sometimes viable. Please state your assumptions. In my comments I have always implicitly assumed the usual vptr/vtable implementation of virtual calls, and separate compilation.Verwoerd
(...) Do you consider the inefficient cfront model a viable choice? In most comment I just rule out cfront approach for the sake of efficiency, but others might disagree. I know many approaches are possible, but I believe that any approach that makes the simplest case (virtual call with non-virtual SI) less efficient is likely to be rejected.Verwoerd
@LokiAstari "your comments are correct at a superficial level but ignore all the real hard work and research done by compiler writers over the last ten years" which real-world compiler tried to implement these research results? If nobody is using this, maybe there is a reason, other than retro-compatibility.Verwoerd
@LokiAstari "You only need one vptr if there is no virtual inheritance. Otherwise you need a vptr per class." This is an extraordinary statement, and you would have to prove it. "You really need to read the book before you make any more comments" no, you need to backup your statements.Verwoerd
@curiousguy: I have not worked on clang or VS and its been 10 years since I looked at gcc. But even back then gcc was using techniques that were beyond the CS101 techniques you are describing. But I am sure that all of them have moved well beyond that now. It took me a while to find but the code you are looking for is: source from gcc 4.7.1: gcc/cp/class.c functions build_primary_vtable() and build_secondary_vtable() all the optimizations dangle from there.Redeeming
PS. I am glad you eventually backtracked and came round to the point I originally made (in the very first post). Now it is time for you to go read a book on the subject so you actually know what you are taking about next time.Redeeming
@LokiAstari I know what I am talking about, thank you. Also, I have not "backtracked" in any way.Verwoerd
@curiousguy: I am sure you do at a CS101 level. Yes you did backtrack. The fact that you are trying to recant with the text above proving the opposite is amusing.Redeeming
@LokiAstari When did I backtrack exactly?Verwoerd
@LokiAstari I rechecked the GCC manual, and it does not mention removing the vptr as you describe. IOW, it seems that you were bluffing. "all the optimizations dangle from there" We were discussing vptr not vtable. Anyway, I do not believe there is any optimization possible there (hint: ABI).Verwoerd
@curiousguy: Sorry that you have not read the book and thus do not understand the possible optimizations that are available. I have provide a link for a book so you can actually learn and the "exact code" in gcc that does the optimizations; I am not sure what else I can do. If you don't understand the code there is not much I can do about that.Redeeming
@LokiAstari "thus do not understand the possible optimizations that are available" No optimization are available, and you are bluffing, and I call your bluff. "I am not sure what else I can do." Describe in plain English the optimizations? "If you don't understand the code there is not much I can do about that." Which code? You are bluffing, again.Verwoerd
@curiousguy: 6 Months later .... " No optimization are available": Yes they are. "Describe in plain English": Sorry I am not going to compress a book into 600 characters. I could suggest some books on compiler optimization techniques that you could read (or you can read the code). As I said above it was 10 years ago that I was in this code base (and even then it was hard) now it is harder. At this point it does not really matter if I am correct or not. Nobody is going to read the comment log this far down. If you choose not to believe me fine I really don't care. Thus you win.Redeeming
@LokiAstari "Thus you win." Indeed. You cannot even give an example of a possible optimization, because any such thing would violate the ABI.Verwoerd
@curiousguy: No. You win because I don't care.Redeeming
D
30

Your question is interesting, however I fear that you are aiming too big as a first question, so I will answer in several steps, if you don't mind :)

Disclaimer: I am no compiler writer, and though I have certainly studied the subject, my word should be taken with caution. There will me inaccuracies. And I am not that well versed in RTTI. Also, since this is not standard, what I describe are possibilities.

1. How to implement inheritance ?

Note: I will leave out alignment issues, they just mean that some padding could be included between the blocks

Let's leave it out virtual methods, for now, and concentrate on how inheritance is implemented, down below.

The truth is that inheritance and composition share a lot:

struct B { int t; int u; };
struct C { B b; int v; int w; };
struct D: B { int v; int w; };

Are going to look like:

B:
+-----+-----+
|  t  |  u  |
+-----+-----+

C:
+-----+-----+-----+-----+
|     B     |  v  |  w  |
+-----+-----+-----+-----+

D:
+-----+-----+-----+-----+
|     B     |  v  |  w  |
+-----+-----+-----+-----+

Shocking isn't it :) ?

This means, however, than multiple inheritance is quite simple to figure out:

struct A { int r; int s; };
struct M: A, B { int v; int w; };

M:
+-----+-----+-----+-----+-----+-----+
|     A     |     B     |  v  |  w  |
+-----+-----+-----+-----+-----+-----+

Using these diagrams, let's see what happens when casting a derived pointer to a base pointer:

M* pm = new M();
A* pa = pm; // points to the A subpart of M
B* pb = pm; // points to the B subpart of M

Using our previous diagram:

M:
+-----+-----+-----+-----+-----+-----+
|     A     |     B     |  v  |  w  |
+-----+-----+-----+-----+-----+-----+
^           ^
pm          pb
pa

The fact that the address of pb is slightly different from that of pm is handled through pointer arithmetic automatically for you by the compiler.

2. How to implement virtual inheritance ?

Virtual inheritance is tricky: you need to ensure that a single V (for virtual) object will be shared by all the other subobjects. Let's define a simple diamond inheritance.

struct V { int t; };
struct B: virtual V { int u; };
struct C: virtual V { int v; };
struct D: B, C { int w; };

I'll leave out the representation, and concentrate on ensuring that in a D object, both the B and C subparts share the same subobject. How can it be done ?

  1. Remember that a class size should be constant
  2. Remember that when designed, neither B nor C can foresee whether they will be used together or not

The solution that has been found is therefore simple: B and C only reserve space for a pointer to V, and:

  • if you build a stand-alone B, the constructor will allocate a V on the heap, which will be handled automatically
  • if you build B as part of a D, the B subpart will expect the D constructor to pass the pointer to the location of V

And idem for C, obviously.

In D, an optimization allow the constructor to reserve space for V right in the object, because D does not inherit virtually from either B or C, giving the diagram you have shown (though we don't have yet virtual methods).

B:  (and C is similar)
+-----+-----+
|  V* |  u  |
+-----+-----+

D:
+-----+-----+-----+-----+-----+-----+
|     B     |     C     |  w  |  A  |
+-----+-----+-----+-----+-----+-----+

Remark now that casting from B to A is slightly trickier than simple pointer arithmetic: you need follow the pointer in B rather than simple pointer arithmetic.

There is a worse case though, up-casting. If I give you a pointer to A how do you know how to get back to B ?

In this case, the magic is performed by dynamic_cast, but this require some support (ie, information) stored somewhere. This is the so called RTTI (Run-Time Type Information). dynamic_cast will first determine that A is part of a D through some magic, then query D's runtime information to know where within D the B subobject is stored.

If we were in case where there is no B subobject, it would either return 0 (pointer form) or throw a bad_cast exception (reference form).

3. How to implement virtual methods ?

In general virtual methods are implemented through a v-table (ie, a table of pointer to functions) per class, and v-ptr to this table per-object. This is not the sole possible implementation, and it has been demonstrated that others could be faster, however it is both simple and with a predictable overhead (both in term of memory and dispatch speed).

If we take a simple base class object, with a virtual method:

struct B { virtual foo(); };

For the computer, there is no such things as member methods, so in fact you have:

struct B { VTable* vptr; };

void Bfoo(B* b);

struct BVTable { RTTI* rtti; void (*foo)(B*); };

When you derive from B:

struct D: B { virtual foo(); virtual bar(); };

You now have two virtual methods, one overrides B::foo, the other is brand new. The computer representation is akin to:

struct D { VTable* vptr; }; // single table, even for two methods

void Dfoo(D* d); void Dbar(D* d);

struct DVTable { RTTI* rtti; void (*foo)(D*); void (*foo)(B*); };

Note how BVTable and DVTable are so similar (since we put foo before bar) ? It's important!

D* d = /**/;
B* b = d; // noop, no needfor arithmetic

b->foo();

Let's translate the call to foo in machine language (somewhat):

// 1. get the vptr
void* vptr = b; // noop, it's stored at the first byte of B

// 2. get the pointer to foo function
void (*foo)(B*) = vptr[1]; // 0 is for RTTI

// 3. apply foo
(*foo)(b);

Those vptrs are initialized by the constructors of the objects, when executing the constructor of D, here is what happened:

  • D::D() calls B::B() first and foremost, to initiliaze its subparts
  • B::B() initialize vptr to point to its vtable, then returns
  • D::D() initialize vptr to point to its vtable, overriding B's

Therefore, vptr here pointed to D's vtable, and thus the foo applied was D's. For B it was completely transparent.

Here B and D share the same vptr!

4. Virtual tables in multi-inheritance

Unfortunately this sharing is not always possible.

First, as we have seen, in the case of virtual inheritance, the "shared" item is positionned oddly in the final complete object. It therefore has its own vptr. That's 1.

Second, in case of multi-inheritance, the first base is aligned with the complete object, but the second base cannot be (they both need space for their data), therefore it cannot share its vptr. That's 2.

Third, the first base is aligned with the complete object, thus offering us the same layout that in the case of simple inheritance (the same optimization opportunity). That's 3.

Quite simple, no ?

Dextro answered 16/4, 2011 at 11:26 Comment(15)
"they both need space for their data" you neglected the case where they have no data (are almost empty)!Verwoerd
@curiousguy: Indeed, with Empty Base Optimization the first base could be optimized out (in the layout) thus allowing the second base to share its v-pointer with the derived object. It's a corner case though, so I'll leave it as is :)Dextro
@MatthieuM. There is a much more particular case: almost-empty base class optimisation (= the class only has a vptr). If two classes have the exact same vtable layout, that is, they have exact same virtual functions in same order (so their vtable, written as a struct, would be layout compatible), they could be called "virtually layout compatible". If 1) two classes B1 and B2 are almost empty (= have no member subobject), 2) are "virtually layout compatible"Verwoerd
(...) and 3) if D derived from B1 and B2 has "equal" final overrider for every (B1, B2) virtual function (virtual function do not have an identity (unlike an ordinary function, you cannot grab the address of a virtually called function), so they are equal iff they are not distinguishable in any execution, not if they have the same definition), then B1 and B2 are "virtually equivalent"; two "virtually equivalent" subobjects are not distinguishable and one can be "elided" (or both can be mixed, or "confused"). So they can use the same vptr. ;)Verwoerd
@curiousguy: Damned. I had never ever thought (or heard) about this case. I would further suppose that B1 being devoid member is sufficient for folding (the members of B2 would come later). Do you have any reference to documentation on the matter ? It's still a bit unclear to me and I'd rather only edit the answer once I have a firm grasp on what's going on.Dextro
@MatthieuM. "I had never ever thought (or heard) about this case." Actually, I was making this up. I don't think there is the slightest chance of any real world benefit.Verwoerd
@MatthieuM. "I would further suppose that B1 being devoid member is sufficient for folding (the members of B2 would come later)." Sharing of vptr is only possible if the vtables are shared; unless you are doing a whole-program compilation, it's difficult to get two independent classes to cooperate this way. If you are doing whole-program translation, then you could possibly reduce the number of vptr in some cases of MI of almost empty classes (so called "interfaces").Verwoerd
@curiousguy: I would say that when compiling D, you do know whether the virtual tables of B1 and B2 are identically layed out (or even if one is a subset of the other) as well as knowing which functions are overriden and thus could, potentially, decide to fold them in a single v-ptr.Dextro
@MatthieuM. "you do know whether the virtual tables of B1 and B2 are identically layed out (or even if one is a subset of the other)" In theory. But I bet this almost never happens in practice. OTOH, if you wanted to do whole program analysis, you could make vtable compatible, by force. (Just to have a few less vptr, I don't know if it's ever worth it.)Verwoerd
@curiousguy: I don't think we would need a whole program analysis. I don't suggest to folder the tables together, just the v-pointer, at the derived instance level (D). Since a base class must be complete, you do know when working on D whether the two pointers can be folded.Dextro
@MatthieuM. I understand. But IMO the real world case that makes folding possible are few: two "interface+default implementation" classes (no non-static data members, only member functions) would have exactly the same virtual members (same signatures) in the same order (actually you could remove the same order requirement by sorting the signature in the ABI). The two classes could still be different: they aren't Java-interfaces, they can have functions implementations, and they can be distinct; but these implementation differences have to be suppressed by the overrider in the derived class.Verwoerd
@MatthieuM. Why would you derive D from two different "interface+default implementation" classes IDI1 and IDI2, when you know you are going to override the default implementation entirely, making the two base strictly classes equivalent? 1) You would need to do that if there are function interfaces using IDI1 and IDI2. This sounds like bad design: the function interface should refer to a base class of IDI1 and IDI2, I with no default implementation. Then D would only derive from I.Verwoerd
@MatthieuM. 2) You would do that in order to show two different, incompatible compile-time views of some class: Ia has non-virtual member Ia::draw() for drawing on the screen, Ib has non-virtual member Ia::draw() for dealing the cards. (Why not "deal_cards" then? With some time, better examples could be found.) Ia and Ib would have the exact same virtual functions, inherited from I. D derives from Ia and Ib. Then nothing prevents the property (Ia*)&d == (Ib*)&d for any d object of type D: the Ia and Ib base classes subobjects of D are runtime equivalents.Verwoerd
@MatthieuM. Have many times have you seen this multi-faced designs? They are certainly useful when an object can have two isomorphic "views". But I would want this multi-view feature to work without the overhead of virtual function calls: the view interfaces Ia and Ib cannot access D directly, they need to use virtual functions. Multi-view can simply be done in a pure C design with internal pointers, and it will be more efficient (by giving away the flexibility of future extension by deriving from these classes).Verwoerd
@curiousguy: Actually, I was thinking of B1 and B2 being abstract classes (thus only specifying an interface, no implementation), and having one being an undeclared subset of the other (in term of interface). I do think it is a corner case and optimizing it is not worth it.Dextro
T
1

If a class has virtual members, one need to way to find their address. Those are collected in a constant table (the vtbl) whose address is stored in an hidden field for each object (vptr). A call to a virtual member is essentially:

obj->_vptr[member_idx](obj, params...);

A derived class which add virtual members to his base class also need a place for them. Thus a new vtbl and a new vptr for them. A call to an inherited virtual member is still

obj->_vptr[member_idx](obj, params...);

and a call to new virtual member is:

obj->_vptr2[member_idx](obj, params...);

If the base is not virtual, one can arrange for the second vtbl to be put immediately after the first one, effectively increasing the size of the vtbl. And the _vptr2 is no more needed. A call to a new virtual member is thus:

obj->_vptr[member_idx+num_inherited_members](obj, params...);

In the case of (non virtual) multiple inheritance, one inherit two vtbl and two vptr. They can't be merged, and calls must pay attention to add an offset to the object (in order for the inherited data members to be found at the correct place). Calls to the first base class members will be

obj->_vptr_base1[member_idx](obj, params...);

and for the second

obj->_vptr_base2[member_idx](obj+offset, params...);

New virtual members can again either be put in a new vtbl, or appended to the vtbl of the first base (so that no offsets are added in future calls).

If a base is virtual, one can not append the new vtbl to the inherited one as it could leads to conflicts (in the example you gave, if both B and C append their virtual functions, how D be able to build its version?).

Thus, A needs a vtbl. B and C need a vtbl and it can't be appended to A's one because A is a virtual base of both. D needs a vtbl but it can be appended to B one as B is not a virtual base class of D.

Thorsten answered 16/4, 2011 at 5:54 Comment(0)
M
0

It all has to do with how compiler figures out the actual addresses of method functions. The compiler assumes that virtual table pointer is located at a known offset from the base of the object (typically at offset 0). The compiler also needs to know the structure of the virtual table for each class - in other words, how to lookup pointers to functions in the virtual table.

Class B and class C will have completely different structures of Virtual Tables since they have different methods. Virtual table for class D can look like a virtual table for class B followed by additional data for methods of class C.

When you generate an object of class D, you can cast it as a pointer to B or as a pointer to C or even as a pointer to class A. You may pass these pointers to modules that are not even aware of existence of class D, but can call methods of class B or C or A. These modules need to know how to locate the pointer to the virtual table of the class and they need to know how to locate pointers to methods of class B/C/A in the virtual table. That's why you need to have separate VPTRs for each class.

Class D is well aware of existence of class B and the structure of its virtual table and therefore can extend its structure and reuse the VPTR from object B.

When you cast a pointer to object D to a pointer to object B or C or A, it will actually update the pointer by some offset, so that it starts from vptr corresponding to that specific base class.

Misdirection answered 16/4, 2011 at 5:54 Comment(0)
N
0

I could not see any reason why there is requirement of separate memory in each class for vptr

At runtime, when you invoke a (virtual) method via a pointer, the CPU has no knowledge about the actual object on which the method is dispatched. If you have B* b = ...; b->some_method(); then the variable b can potentially point at an object created via new B() or via new D() or even new E() where E is some other class that inherits from (either) B or D. Each of these classes can supply its own implementation (override) for some_method(). Thus, the call b->some_method() should dispatch the implementation from either B, D or E depending on the object on which b is pointing.

The vptr of an object allows the CPU to find the address of the implementation of some_method that is in effect for that object. Each class defines it own vtbl (containing addresses of all virtual methods) and each object of the class starts with a vptr that points at that vtbl.

Naashom answered 16/4, 2011 at 6:0 Comment(0)
G
0

I think D needs 2 or 3 vptrs.

Here A may or may not require a vptr. B needs one that should not be shared with A (because A is virtually inherited). C needs one that should not be shared with A (ditto). D can use B or C's vftable for its new virtual functions (if any), so it can share B's or C's.

My old paper "C++: Under the Hood" explains the Microsoft C++ implementation of virtual base classes. http://www.openrce.org/articles/files/jangrayhood.pdf

And (MS C++) you can compile with cl /d1reportAllClassLayout to get a text report of class memory layouts.

Happy hacking!

Griselgriselda answered 27/6, 2011 at 22:1 Comment(1)
If you need the vptr to locate the virtual base subobject, you cannot use the virtual base subobject vptr as your vptr.Verwoerd

© 2022 - 2024 — McMap. All rights reserved.