RSS

Introduction to Virtual Inheritance:

Inheritance is one of the most important concepts of object oriented programming and it is also present in C++ (added from Simula, where it was first invented in 1967).

Inheritance is used to create new classes, called derived classes, that inherit methods and properties from existing ones, called base classes. It provides a very efficient method of code reuse, giving the ability to create new classes that implement a common interface or use the functionality of an existing class in your own implementation. When more than one class fitting your implementation, you can join their functionality in your class by inheriting from all of them. This feature is allowed by the C++ standard and it's called multiple inheritance.



The diamond problem:

One of the problems that arises due to multiple inheritance is the diamond problem. A classical illustration of this is given by Bjarne Stroustrup (the creator of C++) in the following example:

class storable //this is the our base class inherited by transmitter and receiver classes
{
public:
storable(const char*);
virtual void read();
virtual void write();
virtual ~storable();
private:
....
}

class transmitter: public storable
{
public:
void write();
...
}

class receiver: public storable
{
public:
void read();
...
}

class radio: public transmitter, public receiver
{
public:
void read();
....
}



Since both transmitter and receiver classes are using the method write() from the base class, when calling the method write() from a radio object we are going to create an ambiguity--that is, the compiler can't know which implementation of write() to use, the one from the transmitter class or the one from the receiver class.

To understand how this is generated, let's take a look at how the objects are represented in memory. Inheritance simply puts the implementation of two objects one after another, but in this case radio is both a transmitter and a receiver, so the storable class gets duplicated inside the radio object. The g++ compiler will complain when compiling the code: error: 'request for member "write" is ambiguous', because it can't figure out either to get the method write() from storable::receiver::radio or from storable::transmitter::radio.

To prevent this behavior, C++ provides the virtual inheritance mechanism. In order to prevent the compiler from giving an error we use the keyword virtual when we inherit from the base class storable in both derived classes:



class transmitter: public virtual storable
{
public:
void read();
...
}

class receiver: public virtual storable
{
public:
void read();
...
}



Making the inheritance virtual will cause the compiler to provide a virtual function table (vtable) for classes transmitter and receiver, which will allow binding methods at run-time. By the way, the use of a vtable is also needed whenever virtual methods are declared, not only for virtual inheritance. The vtable is created in order to maintain the offset between the storable class and the beginning of transmitter/receiver for runtime binding. This will result in two new virtual pointers (vptr) in the memory layout of class radio, one pointing to the transmitter part and one to the receiver (there is a vptr for every virtual base class). Now the derived classes transmitter and receiver share the implementation of storable, making the implementation of radio unambiguous, and the code will compile fine.

The diamond problem is solved in many ways in different programming languages--for example, in Java and C#, full multiple inheritance is not allowed. Instead, they impose some constraints: allowing inheritance only from multiple interfaces (an interface is equivalent with an abstract base in C++, a class with virtual pure members).


Delegating to a sister class:

A powerful technique that arises from using virtual inheritance is to delegate a method from a class in another class by using a common abstract base class. This is also called cross delegation. Let's assume we have a similar scenario like in the diamond example, with small changes. Suppose the write() method in transmitter class needs to access the read() method from receiver for the radio to work (this is not a usual expected behavior, but let's suppose this for the sake of illustration) :


class storable
{
public:
storable(const char*);
virtual void read()=0; //this becomes pure virtual making storable an abstract
virtual void write(); //class
virtual ~storable();
private:
....
}

class transmitter: public virtual storable
{
public:
void write()
{
read();
....
}
}

class receiver: public virtual storable
{
public:
void read();
}

class radio: public transmitter, public receiver
{
public:
...
}

int main()
{
radio *rad = new radio();
receiver *r1 = rad;
transmitter *r2 =rad;

rad->write();
r1->write();
r2->write();
return 1;
}


Because of virtual inheritance, when the write() function from the transmitter class is called, the method read() from the receiver class gets called (as you may have noticed, the transmitter class doesn't have a read() function). In the above hierarchy we can instantiate only the radio class because transmitter and receiver are abstract due to virtual inheritance.


Other considerations when using multiple inheritance in C++:


When creating a derived object like radio, constructors from the hierarchy, the object is built in a specific order: first a pointer to the vtable is created and initialized, then the virtual base class constructor is run (but only once), then constructors from non-virtual bases are run, then the runtime calls constructors for the data members in the order they are declared, and finally, the body of the constructor is executed. In the case of destruction these steps are performed in reverse order--virtual base classes that appear in the hierarchy are destroyed last.

  • Digg
  • Del.icio.us
  • StumbleUpon
  • Reddit
  • RSS

0 comments:

Post a Comment