Virtual inheritance
Now we've come to one of most complicated features - virtual inheritance. Let's start as usual, with most trivial example:
class Base
{
public:
int m_base_member;
void set_base_member(int x)
{
m_base_member = x;
}
};
class Derived : public virtual Base
{
public:
int m_derived_member;
void set_derived_member(int x)
{
m_derived_member = x;
}
};
This time I've added implementations to methods, as they do matter now. Base class translates to this (nothing particular here):
struct Base
{
int m_base_member;
};
void Base_set_base_member(Base *_this, int x)
{
_this->m_base_member = x;
}
Now let's see the layout for Derived:
struct Derived_SubObject
{
int m_derived_member;
};
struct Derived : public virtual Base
{
void *_vtable;
Base _parent;
Derived_SubObject _derived_part;
};
One thing is, that Derived class does have a VTable. That could have been guessed. As you can see, I've split Derived class specific members into a separate struct, that I placed directly inside Derived, rather than inlining members. This was done in order to explain the code in set_derived_member() method, which looks like this:
void Derived_set_derived_member(Derived *_this, int x)
{
Derived_SubObject *_derived_part = _get_sub_object(_this, Derived_part);
_derived_part->m_derived_member = x;
}
The important part here is that implementation of set_derived_member() make no assumptions about layout of Derived being passed in, with exception for VTable, which is expected to be at the start. The sub-part of Derived can be located anywhere in the object, method always looks it up via VTable. This has two implications:
- negative performance impact, as instead of accessing variable via offset in object, a VTable lookup is performed
- in case of multiple inheritance, duplication can be avoided for diamond-problem (as shown below)
class Base
{
public:
int m_base_int;
};
class Derived1 : public virtual Base
{
public:
int m_derived1_int;
void foo(int x) { m_base_int = x; }
};
class Derived2 : public virtual Base
{
public:
bool m_derived2_bool;
void bar(int y) { m_base_int = y; }
};
class DerivedMultiple : public Derived1, public Derived2
{
public:
bool m_multiple_bool;
};
/* DerivedMultiple object; */
Derived1 *d1 = &object;
d1->m_base_int = 3;
d1->foo(6);
Derived2 *d2 = &object;
d2->m_base_int = 4;
d2->bar(2);
Base *b = &d2;
b->m_base_int = 0;
The resulting structs and functions are:
struct Base
{
int m_base_int;
};
struct Derived1_SubObject
{
int m_derived1_int;
};
struct Derived1
{
void *_vtable;
Base _parent;
Derived1_SubObject _derived1_part;
};
void Derived1_foo(Derived1 *_this, int x)
{
Base *base = _get_sub_object(_this, Base_part);
base->m_base_int = x;
}
struct Derived2_SubObject
{
bool m_derived2_bool;
};
struct Derived2
{
void *_vtable;
Base _parent;
Derived2_SubObject _derived2_part;
};
void Derived2_bar(int y)
{
Base *base = _get_sub_object(_this, Base_part);
base->m_base_int = y;
}
struct DerivedMultiple_Subobject
{
bool m_multiple_bool;
};
struct DerivedMultiple
{
void *_vtable;
Base _parent;
Derived1_SubObject _derived1_part;
Derived2_SubObject _derived2_part;
DerivedMultiple_Subobject _derivedmultiple_part;
};
As you see, in the final layout of DerivedMultiple there is only one Base part. The methods Derived1::foo() and Derived2::bar() have identical code, all they require is a VTable at the start of _this. Because DerivedMultiple satisfies this requirement, it can be passed in directly to either of two.Let's analyze the code part step by step:
/* DerivedMultiple object; */ Derived1 *d1 = &object; _get_sub_object(d1, Base_part)->m_base_int = 3; // d1->m_base_int = 3; Derived1_foo(d1, 6); // d1->foo(6);When doing pointer assignment compiler is not required to do anything about pointer, as all that is needed is a VTable at the start. When accessing anything from Base a VTable-lookup is done to obtain a pointer to Base part inside object. foo() call is trivial.
The code where we use Derived2 is pretty much the same:
Derived2 *d2 = &object; _get_sub_object(d2, Base_part)->m_base_int = 4; //d2->m_base_int = 4; Derived2_bar(d2, 2); //d2->bar(2);Last, let's cast to Base and use that:
Base *b = _get_sub_object(d2, Base_part); b->m_base_int = 0;The base is obtained the same way as inside implementations of foo() and bar(). The important thing to note here is that making Base into a virtual class does not change that, except that pointer to VTable could be reused then. This draws few conclusions:
- Virtual inheritance has negative performance impact, because of regular sub-object lookups
- In case of multiple inheritance and diamond hierarchy, two copies of common base class can be avoided
- Internal layout of class is unpredictable, compiler can rearrange sub-objects
- Virtual inheritance may not solve problem with duplicate base class; this can happen, if there is a complex mix of classes, where some classes do not use virtual inheritance, so their layout has to be preserved
Komentarų nėra:
Rašyti komentarą