Vishal Sahu
Vishal Sahu

Reputation: 780

choosing vptr in case of multiple inheritance

This is similar to many previous questions, but it asks something which I was not able to find answer.

#include <iostream>
using namespace std;

class Base1 {
    public:
        int b1_data;
        virtual void b1_fn() {cout << "I am b1\n";}
};
class Base2 {
    public:
        int b2_data;
        virtual void b2_fn() {cout << "I am b2\n";}
};
class Derived : public Base1, public Base2 {
    public:
        int d_data;
        void b1_fn() {cout << "I am b1 of d\n";}
        void b2_fn() {cout << "I am b2 of d\n";}
};

int main() {
    Derived *d = new Derived();
    Base1 *b1 = d;
    /*My observation mentioned below is implementation dependant, for learning,
    I assume, there is vtable for each class containing virtual function and in
    case of multiple inheritance, there are multiple vtables based on number of
    base classes(hence that many vptr in derived object mem layout)*/

    b1->b1_fn(); // invokes b1_fn of Derived because d points to 
                 // start of d's memory layout and hence finds vtpr to
                 // Derived vtable for Base1(this is understood)
    Base2 *b2 = d;
    b2->b2_fn(); // invokes b2_fn of Derived but how? I know that it "somehow" 
                 // gets the offset added to d to point to corresponding Base2 
                 // type layout(which has vptr pointing to Derived vtable for 
                 // Base2) present in d's memory layout. 
    return 0;
}

Specifically, how does b2 point to vptr for vtable of Derived for Base2 to get to b2_fn()? I've tried seeing memlayout dump from gcc but couldn't figure out much.

Upvotes: 4

Views: 821

Answers (2)

user265906
user265906

Reputation: 170

#include <iostream>

using namespace std;


class Base1 {
public:
    virtual void b1_fn() {cout << "I am b1\n";}
};
class Base2 {
public:
    virtual void b2_fn() {cout << "I am b2\n";}
};
class Derived : public Base1, public Base2 {
public:
    void b1_fn() {cout << "I am b1 of d\n";}
    void b2_fn() {cout << "I am b2 of d\n";}
};

int main() {


Base1 b1;
Base2 b2;
Derived d;

cout<<"size of base1 object"<<sizeof(b1)<<endl;
cout<<"size of base2 object"<<sizeof(b2)<<endl;
cout<<"size of derived object"<<sizeof(d)<<endl;
return 0;

}

Output of above program is: 8 8 16

So you can see that derived object has inherited two vptr from base1 and base2

both vptr in derived class points to vtable of derived class. since you have overriden the base methods in derived class so vptr will point to the derived class methods.

In case you had not overriden methods in derived class than vtable would be having address of base class methods and vptr would be pointing to those

Thats why b2->b2_fn(); calls derived method

Upvotes: 0

Christophe
Christophe

Reputation: 73446

The compiler in case of multiple inheritance, construct his vtables so that every subobject has an appropriate vtable. Of course this is implementation dependent (as the vtables themselves), but it would be organized like that:

  • A Base1 object has a vptr pointing to a vtable containing a unique pointer to Base1::b1_fn
  • A Base2 object has a vptr pointing to a vtable containing a unique pointer to Base2::b2_fn
  • A Derived object has a vptr pointing to a vtable that starts with the vtable layout corresponding to Base1, but extendeds it with the missing elements of Base2's vtable. With "Layout" I mean that the pointer for b1_fn() is at the same offset, but it might point to an overriding function. So, here, the table would contain Derived::b1_fn followed by Derived::b2_fn. This combined layout ensures that the Base1 subobject in Derived can share the vtable with its child.
  • But a Derived object is composed of 2 subobjects: so the Base1 subobject will be followed by a the Base2 subobject, which will have its own vtable, using the layout required for Base2, but again with Base2::b2_fn instead of the original one.

When casting the Derived pointer to a Base2 pointer, the compiler will make it point to the Base2 subobject with its vtable, by applying a fixed offset determined at compile time.

By the way if you would do a downcasting, the compiler would similarly use the fixed offset in the other direction, to find the start of the Derived. This is all pretty simple until you use virtual bases, in which the technique of the fixed offset can no longer work. Virtual base pointers must then be used as explained in this other SO answer

This Dr.Dobb's article is the best explanation for all these layouts with some good pictures.

Upvotes: 4

Related Questions