4

while trying to analyse in greater depth inheritance mechanism of C++ I stumbled upon the following example:

#include<iostream>

using namespace std;

class Base {
public:
    virtual void f(){
    cout << "Base.f" << endl; 
    }
};

class Left : public virtual Base {    
};

class Right : public virtual Base{
public:
    virtual void f(){
        cout << "Right.f" << endl; 
    }
};

class Bottom : public Left, public Right{

};

int main(int argc,char **argv)
{
    Bottom* b = new Bottom();
    b->f();
}

The above, somehow, compiles and calls Right::f(). I see what might be going on in the compiler, that it understands that there is one shared Base object, and that Right overrides f(), but really, in my understanding, there should be two methods: Left::f() (inherited from Base::f()) and Right::f(), which overrides Base::f(). Now, I would think, based that there are two separate methods being inherited by Bottom, both with same signature, there should be a clash.

Could anyone explain which specification detail of C++ deals with this case and how it does it from the low-level perspective?

Bober02
  • 15,034
  • 31
  • 92
  • 178

1 Answers1

4

In the dreaded diamond there is a single base, from which the two intermediate objects derive and then the fourth type closes the diamond with multiple inheritance from both types in the intermediate levels.

Your question seems to be how many f functions are declared in the previous example? and the answer is one.

Lets start with the simpler example of a linear hierarchy of just base and derived:

struct base {
   virtual void f() {}
};
struct derived : base {
   virtual void f() {}
};

In this example there is a single f declared for which there are two overrides, base::f and derived::f. In an object of type derived, the final overrider is derived::f. It is important to note that both f functions represent a single function that has multiple implementations.

Now, going back to the original example, on the line on the right, Base::f and Right::f are in the same way the same function that is overridden. So for an object of type Right, the final overrider is Right::f. Now for a final object of type Left, the final overrider is Base::f as Left does not override the function.

When the diamond is closed, and because inheritance is virtual there is a single Base object, that declares a single f function. In the second level of inheritance, Right overrides that function with its own implementation and that is the final overrider for the most derived type Bottom.

You might want to look at this outside of the standard and take a look at how this is actually implemented by compilers. The compiler, when creating the Base object it adds a hidden pointer vptr to the virtual table. The virtual table holds pointers to thunks (for simplicity just assume that the table held pointers to the function's final overriders, [1]). In this case, the Base object will contain no member data and just a pointer to a table that holds a pointer to the function Base::f.

When Left extends Base, a new vtable is created for Left and the pointer in that vtable is set to the final overrider of f at this level, which is incidentally Base::f so the pointers in both vtables (ignoring the trampolin) jump to the same actual implementation. When an object of type Left is being constructed, the Basesubobject is initialized first, and then prior to initialization of the members of Left (if there were) the Base::vptr pointer is updated to refer to Left::vtable (i.e. the pointer stored in Base refers to the table defined for Left).

On the other side of the diamond, the vtable that is created for Right contains a single thunk that ends up calling Right::f. If an object of type Right was to be created the same initialization process would happen and the Base::vptr would point to Derived::f.

Now we get to the final object Bottom. Again, a vtable is generated for the type Bottom and that vtable, as is the case in all others, contains a single entry that represents f. The compiler analyzes the hierarchy of inheritance and determines that Right::f overrides Base::f, and there is no equivalent override on the left branch, so in Bottom's vtable the pointer representing f refers to Right::f. Again, during construction of the Bottom object, the Base::vptr is updated to refer to Bottom's vtable.

As you see, all four vtables have a single entry for f, there is a single f in the program, even if the value stored in each vtable is different (the final overriders differ).

[1] The thunk is a small piece of code that adapts the this pointer if needed (multiple inheritance usually implies it is needed) and then forwards the call to the actual override. In the event of single inheritance, the this pointer does not need to be updated and the thunk disappears, with the entry in the vtable pointing directly to the actual function.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • OK, so `Bottom` object closes the diamond and it looks at Base class which has function f() defined. Now there exist two implementations Base::f() and Right::f() and now based on interface hierarchy RIght::f() is chosen. Is that precise? – Bober02 Apr 15 '12 at 23:19
  • @Bober02: It is close enough for all purposes other than implementing the compiler yourself :) I have updated the answer with details of implementation that you might want to take a look at. – David Rodríguez - dribeas Apr 15 '12 at 23:50
  • You mention in your answer that when Left is created its base object vtable points to Left vtable. Is that because there is only one virtual method? If there was a non-virtual method also in Base, it would have a separate vtable right? – Bober02 Apr 16 '12 at 10:08
  • Also, you say tthere is one f function, but different overriders. I see only 2: Base::f as well as Right::f – Bober02 Apr 16 '12 at 10:11
  • @Bober02: Non-virtual functions don't affect the vtable. I don't recall the exact details, but you can assume that there is a single vtable for each type. At each level of the hierarchy, the type will have it's own vtable and the pointer in base will refer to that. If the derived type adds more virtual functions, then the derived vtable will have more entries, but the first entries in the derived vtable will refer to the same virtual functions as that of the base vtable. I am not sure whether a type can have multiple vtables (I think it can) under some circumstances as multiple inheritance. – David Rodríguez - dribeas Apr 16 '12 at 12:54
  • @DavidRodríguez-dribeas There are at least as many vtables as polymorphic base classes. – curiousguy Aug 06 '15 at 08:55
  • @curiousguy: Yes, I omitted some detail: during the process of construction (and destruction) of the complete type, the dynamic type of the object changes and for that the compiler may need to generate different vtables. After the complete object is fully constructed and before the destruction starts, the `vptr` refers to the same `vtable` for all fully constructed instances of the type, that is the "single" `vtable` I mentioned that you can assume. This is also dependent on the ABI, so I can imagine VS and Itanium having different sets – David Rodríguez - dribeas Aug 06 '15 at 09:00
  • @DavidRodríguez-dribeas Right, on construction the dynamic type changes; but a derived class will also contain more than one vptr when virtual or multiple non-virtual inheritance is involved, hence more than one vtable. – curiousguy Aug 06 '15 at 09:37
  • @David Rodríguez - dribeas ***It is important to note that both f functions represent a single function that has multiple implementations.*** How to understand that in the right way? – John Jun 18 '22 at 04:31