5

I'm reading Bjarne's paper: Multiple Inheritance for C++.

In section 3, page 370, Bjarne said that "The compiler turns a call of a member function into an "ordinary" function call with an "extra" argument; that "extra" argument is a pointer to the object for which the member function is called."

I'm confused by the extra this argument. Please see the following two examples:

Example 1:(page 372)

class A {
    int a;
    virtual void f(int);
    virtual void g(int);
    virtual void h(int);
};
class B : A {int b; void g(int); };
class C : B {int c; void h(int); };

A class c object C looks like:

C:

-----------                vtbl:
+0:  vptr -------------->  -----------
+4:  a                     +0: A::f
+8:  b                     +4: B::g
+12: c                     +8: C::h
-----------                -----------  

A call to a virtual function is transformed into an indirect call by the compiler. For example,

C* pc;
pc->g(2)

becomes something like:

(*(pc->vptr[1]))(pc, 2)

The Bjarne's paper told me the above conclusion. The passing this point is C*.

In the following example, Bjarne told another story which totally confused me!


Example 2:(page 373)

Given two classes

class A {...};
class B {...};
class C: A, B {...};

An object of class C can be laid out as a contiguous object like this:

pc-->          ----------- 
                  A part
B:bf's this--> -----------  
                  B part
               ----------- 
                  C part
               -----------

Calling a member function of B given a C*:

C* pc;
pc->bf(2); //assume that bf is a member of B and that C has no member named bf.

Bjarne wrote: "Naturally, B::bf() expects a B* (to become its this pointer)." The compiler transforms the call into:

bf__F1B((B*)((char*)pc+delta(B)), 2);

Why here we need a B* pointer to be the this? If we just pass a *C pointer as the this, we can still access the members of B correctly I think. For example, to get the member of class B inside B::bf(), we just need to do something like: *(this+offset). this offset can be known by the compiler. Is this Right?


Follow up questions for example 1 and 2:

(1) When it's a linear chain derivation (example 1), why the C object can be expected to be at the same address as the B and in turn A sub-objects? There is no problem to use a C* pointer to access class B's members inside the function B::g in example 1? For example, we want to access the member b, what will happen in runtime? *(pc+8)?

(2) Why can we use the same memory layout (linear chain derivation) for the multiple-inheritance? Assuming in example 2, class A, B, C have exactly the same members as the example 1. A: int a and f; B: int b and bf (or call it g); C: int c and h. Why not just use the memory layout like:

 -----------               
+0:  a                     
+4:  b                    
+8: c                     
-----------   

(3) I've wrote some simple code to test the differences between the linear chain derivation and multiple-inheritance.

class A {...};
class B : A {...};
class C: B {...};
C* pc = new C();
B* pb = NULL;
pb = (B*)pc;
A* pa = NULL;
pa = (A*)pc;
cout << pc << pb << pa

It shows that pa, pb and pc have the same address.

class A {...};
class B {...};
class C: A, B {...};
C* pc = new C();
B* pb = NULL;
pb = (B*)pc;
A* pa = NULL;
pa = (A*)pc;

Now, pc and pa have the same address, while pb is some offset to pa and pc.

Why the compile make these differences?


Example 3:(page 377)

class A {virtual void f();};
class B {virtual void f(); virtual void g();};
class C: A, B {void f();};
A* pa = new C;
B* pb = new C;
C* pc = new C;
pa->f();
pb->f();
pc->f();
pc->g()

(1) The first question is about pc->g() which relates to the discussion in example 2. Does the compile do the following transformation:

pc->g() ==> g__F1B((*B)((char*)pc+delta(B)))

Or we have to wait for the runtime to do this?

(2) Bjarne wrote: On entry to C::f, the this pointer must point to the beginning of the C object (and not to the B part). However, it is not in general known at compile time that the B pointed to by pb is part of a C so the compiler cannot subtract the constant delta(B).

Why we cannot know the B object pointed to by pb is part of a C at the compile time? Based on my understanding, B* pb = new C, pb points to a created C object and C inherits from B, so a B pointer pb points to part of C.

(3) Assume that we do not know B pointer to by pb is part of a C at the compile time. So we have to store the delta(B) for the runtime which is actually stored with the vtbl. So the vtbl entry now looks like:

struct vtbl_entry {
    void (*fct)();
    int  delta;
}

Bjarne wrote:

pb->f() // call of C::f:
register vtbl_entry* vt = &pb->vtbl[index(f)];
(*vt->fct)((B*)((char*)pb+vt->delta)) //vt->delta is a negative number I guess

I'm totally confused here. Why (B*) not a (C*) in (*vt->fct)((B*)((char*)pb+vt->delta))???? Based on my understanding and Bjarne's introduction at the first sentence at 5.1 section an 377 page, we should pass a C* as this here!!!!!!

Followed by the above code snippet, Bjarne continued writing: Note that the object pointer may have to be adjusted to po int to the correct sub-object before looking for the member pointing to the vtbl.

Oh, Man!!! I totally have no idea of what Bjarne tried to say? Can you help me explain it?

Fihop
  • 3,127
  • 9
  • 42
  • 65

4 Answers4

3

Bjarne wrote: "Naturally, B::bf() expects a B* (to become its this pointer)." The compiler transforms the call into:

bf__F1B((B*)((char*)pc+delta(B)), 2);

Why here we need a B* pointer to be the this?

Consider B in isolation: the compiler needs to be able to compile code ala B::bf(B* this). It doesn't know what classes might be further derived from B (and the introduction of derived code might not happen until long after B::bf is compiled). The code for B::bf won't magically know how to transform a pointer from some other type (e.g. C*) to a B* it can use to access data members and RunTime Type Info (RTTI / virtual dispatch table, typeinfo).

Instead, the caller has the responsibility of extracting a valid B* to the B sub-object in whatever actual runtime type is involved (e.g. C). In this case, the C* holds the address of the start of the overall C object which likely matches the address of the A sub-object, and the B sub-object is some fixed but non-0 offset further into memory: it's that offset (in bytes) that must be added to the C* in order to get a valid B* with which to call B::bf - that adjustment is done when the pointer is cast from C* type to B* type.

(1) When it's a linear chain derivation (example 1), why the C object can be expected to be at the same address as the B and in turn A sub-objects? There is no problem to use a C* pointer to access class B's members inside the function B::g in example 1? For example, we want to access the member b, what will happen in runtime? *(pc+8)?

Linear derivation B : A and C : B can be thought of as successively tacking B-specific fileds on the end of A, then C-specific fields on the end of B (which is still B-specific fields tacked on the end of A). So the whole thing looks like:

[[[A fields...]B-specific-fields....]C-specific-fields...]
 ^
 |--- A, B & C all start at the same address

Then, when we talk about a "B" we're talking about all the embedded A fields as well as the additions, and for "C" there's still all the A and B fields: they all start at the same address.

Regarding *(pc+8) - that's right (given the understanding that we're adding 8 bytes to the address, and not the usual C++ behaviour of adding multiples of the pointee's size).

(2) Why can we use the same memory layout (linear chain derivation) for the multiple-inheritance? Assuming in example 2, class A, B, C have exactly the same members as the example 1. A: int a and f; B: int b and bf (or call it g); C: int c and h. Why not just use the memory layout like:

-----------               
+0:  a                     
+4:  b                    
+8: c                     
-----------   

No reason - that's exactly what happens... the same memory layout. The difference is that the B subobject doesn't consider A to be a part of itself. It's now like this:

[[A fields...][B fields....]C-specific-fields...]
 ^             ^
 \ A&C start   \ B starts

So when you call B::bf it wants to know where the B object starts - the this pointer you provide should be at "+4" in the above list; if you call B::bf using a C* then the compiler-generated calling code will need to add that 4 in to form the implicit this paramter to B::bf(). B::bf() can't simply be told where A or C start at +0: B::bf() knows nothing about either of those classes and doesn't know how to reach b or its RTTI if you give it a pointer to anything other than its own +4 address.

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
  • Tony, two more questions. Please! Thanks. – Fihop Jun 10 '15 at 06:40
  • @FihopZz: some further explanation added. – Tony Delroy Jun 10 '15 at 08:46
  • Tony, Thanks so much. It's much clearer now. For the second question, let's say two pointers *C pc and *B pb point to A start and B start separately, to access the member b in class B, we need *(pc+4) and *(pb)? How does the compiler or runtime know the C* pointer should add offset 4 while the B* pointer just adds a zero? – Fihop Jun 10 '15 at 14:13
  • @FihopZz: you're welcome. I assume your question above is for the multiple inheritance scenario: *"need `*(pc+4)` and `*(pb)`?"* - that's right. *"How does the compiler know..."* - it's its job to know - it has to create tables of the offsets of data members in the different classes in the program, and use those tables when generating actual machine code. – Tony Delroy Jun 11 '15 at 01:28
2

Maybe this makes more sense if you ignore the function call for now and instead consider the conversion of a C* to a B* that is required before calling bf(). Since the B subobject doesn't start at the same address as the C object, the address needs to be adjusted. In cases where you have only one baseclass, the same is done, but the offset (delta(B)) is zero, so it is optimized out. Then, only the type attached to the address is changed.

BTW: Your quoted code (*((*pc)[1]))(pc, 2) doesn't perform this conversion, which is formally wrong. Since it isn't real code anyway, you have to infer that by reading between the lines. Maybe Bjarne just intended to use the implicit conversion to baseclass there.

BTW 2: I think you misunderstand the layout of classes with virtual functions. Also, just as disclaimer, the actual layouts depend on the system, i.e. compiler and CPU. Anyhow, consider two classes A and B with a single virtual function:

class A {
    virtual void fa();
    int a;
};
class B {
    virtual void fb();
    int b;
};

The layout would then be:

-----------                ---vtbl---
+0:  vptr -------------->  +0: A::fa
+4:  a                     ----------  
-----------                

and

-----------                ---vtbl---
+0:  vptr -------------->  +0: B::fb
+4:  b                     ----------  
-----------                

In words, there are three guarantees for class A (those for B are equivalent):

  • Given a pointer A*, at offset zero to that pointer I find the address of the vtable. At position zero of that table, I find the address of the function fa() for that object. While the actual function may change in derived classes (due to overrides), the offset in the table is fixed.
  • The type of the function in the vtable is fixed, too. At position zero of the vtable is a function that takes a hidden A* this as parameter. The actual function may be overridden in a derived class, but the type of the function here must be retained.
  • Given a pointer A*, at offset four to that pointer I find the value of the member variable a.

Now, consider a third class C:

class C: A, B {
    int c;
    virtual void fa();
};

Its layout would be like

-----------                ---vtbl---
+0:  vptr1 ------------->  +0: A::fa
+4:  a                     
+8:  vptr2 ------------->  +4: B::fb
+12: b                     +8: C::fc
+16: c                     ----------  
-----------

Yes, this class contains two vtable pointers! The reason is simple: The layout of classes A and B is fixed when they are compiled, see above guarantees. In order to allow substituting a C for an A or B (Liskov Substitution Principle), these layout guarantees must be retained, since the code handling the object only knows about e.g. A, but not C.

Some remarks on this:

  • Above, you already find an optimization, the vtable pointer for class C has been merged with the that for class A. This simplification is only possible for one of the baseclasses, hence the difference between single and multiple inheritance.
  • When calling fb() on an object of type C, the compiler must call B::fb with a pointer so that the guarantees above are met. For that, it has to adjust the address of the object so that it points to a B (offset +8) before calling the function.
  • If C overrides fb(), the compiler will generate two versions of that function. One version is for the vtable of the B subobject, which then takes an B* this as hidden parameter. The other will be for the separate entry in the vtable of the C class and it takes a C*. The first one will only adjust the pointer from the B subobject to the C object (offset -8) and call the second one.
  • The above three guarantees are not necessary. You could also store the offset of the member variables a and b inside the vtable. Similarly, the adjustment of the address during function call could be done indirectly via information embedded inside the object via its vtable. This would be much less efficient though.
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
  • Bjarne's *"becomes something like:"* allows room for pseudo-code, and as there's a linear derivation chain rather than multiple-inheritance, the C object can be expected to be at the same address as the B and in turn A sub-objects, so there's no actual pointer adjustment expected. (I expect you know all that, but for other readers - just my perspective on why an explicit cast wouldn't add a lot to the average reader's understanding). – Tony Delroy Jun 10 '15 at 05:56
  • @Tony D, so the linear derivation chain has a different memory layout from the multiple-inheritance? – Fihop Jun 10 '15 at 06:18
  • @FihopZz: yes... in the linear derivation the A, B and C subobjects typically all start at the same memory address; with multiple-inheritance, A and C still have the same address, but B is after A / at some non-0 offset within C. I see from your updated question you've observed this in testing. – Tony Delroy Jun 10 '15 at 08:11
  • I've updated the question a little bit. Ulrich, can you help me take a look at it when you have time – Fihop Jun 10 '15 at 17:52
  • I'm not sure if that's a help to you, but consider taking a look at the "Design and Evolution of C++", I think this should clarify a lot of things. Also, consider taking a course in assembly language. Many of the features of C and hence C++ are mere simplifications of assembly language. Knowing these things (and mentally translating between the two) makes the answer to your questions almost obvious. – Ulrich Eckhardt Jun 10 '15 at 20:16
  • @UlrichEckhardt. That helps a lot!!! Thanks! One more question, what if in the derived class `C`, there are two virtual overridden functions `fa` and `fb`, what's the layout of an object of C? – Fihop Jun 11 '15 at 14:45
  • @UlrichEckhardt, one more question, so there is only one virtual table for the object `C` – Fihop Jun 11 '15 at 15:00
2

Function bf() in your example is a member of class B. Inside B::bf() you will be able to access all members of B. That access is performed through this pointer. So in order for that access to work properly, you need this inside B::bf() to point precisely to B. This is why.

The implementation of B::bf() does not know whether this B object is a standalone B object, or a B object embedded into C object, or some other B object embedded into something else. For that reason, B::bf() cannot perform any pointer corrections for this. B::bf() expects all pointer corrections to be done in advance, so that when B::bf() begins execution, this points precisely to B and nowhere else.

This means that when you are calling pc->bf(), you have to adjust the value of pc by some fixed offset (offset of B in C) and use the resultant value as this pointer for bf().

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • I have two more questions. Can you help me again? Thanks – Fihop Jun 10 '15 at 06:40
  • If we just pass a *C pointer as the `this`, we can still access the members of B correctly I think. For example, to get the member of class B inside B::bf(), we just need to do something like: *(this+offset). this offset can be known by the compiler. Is this OKay? Update the original question. – Fihop Jun 10 '15 at 14:36
  • @FihopZz: As I stated in my answer (and other answers), when we are already *inside* `B::bf()`, *we have no idea* whether our `B` is embedded into `C` (as in your code) or our `B` has nothing to do with `C` at all. Remember that `B` can be a standalone object, like `B b;`. Because we don't know that, we don't know whether we should add some `offset` to `this` or not. We have no way of knowing that inside `B::bf()`. To solve this problem, the task of supplying the *already adjusted* `this` value is placed onto the calling code. – AnT stands with Russia Jun 10 '15 at 14:43
  • By the time we get into `B::bf()` pointer `this` must be already properly adjusted by the calling code and point exactly to `B`. That way we don't have to worry about adding any `offset` to `this`. – AnT stands with Russia Jun 10 '15 at 14:47
  • Thanks so much, AnT. It's totally clear now. I've updated the question a little bit. Can you help me take a look at when you are free. – Fihop Jun 10 '15 at 17:51
  • AnT, one more question. I assume that the compile or some runtime executors use the offset implementation to get the members of class? There must exist an offset table created by the compiler or something else? Still this example, `A` has a `int a`, `B` has a `int b`, `C` has a `int c`. so we have `0: a, 4: b, 8: c` this kind of table? Then a `C` pointer, like pc can access the member `b` by checking the table and then get *(pc+4). But what if a `B` pointer `pb`? `pb` wants to access his own member `b`. And then looking the table, ooh, the offset is 4??? – Fihop Jun 10 '15 at 18:05
-1

It should in theory be that the compiler would take any this's in the code and if refer to the pointer so it knows what the this is referring to.

pbcub1
  • 10
  • 3
  • sorry, I do not get it. Can you explain a little bit more detail? – Fihop Jun 10 '15 at 05:29
  • Well I have not read his paper and I don't know for sure however I would assume it would be as if someone was asking "What is `this`?" you look at him confused, what is what? Unless that person is grabbing or *pointing* to something, you don't know what he is referring to as `this`. Therefore my point is that that other argument that you are talking about would be the finger pointing to the object or the hand grabbing the object. I would assume it would be so the compiler could make sense of `this`. – pbcub1 Jun 10 '15 at 05:33
  • The question is about what the compiler does, not what it can make sense of. – user207421 Jun 10 '15 at 05:45
  • @EJP, I've updated the question a little bit. Ulrich, can you help me take a look at it when you have time. – Fihop Jun 10 '15 at 17:52