Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does virtual inheritance need a vtable even if no virtual functions are involved?

I read this question: C++ Virtual class inheritance object size issue, and was wondering why virtual inheritance results in an additional vtable pointer in the class.

I found an article here: https://en.wikipedia.org/wiki/Virtual_inheritance

which tells us:

However this offset can in the general case only be known at runtime,...

I don't get what is runtime-related here. The complete class inheritance hierarchy is already known at compile time. I understand virtual functions and the use of a base pointer, but there is no such thing with virtual inheritance.

Can someone explain why some compilers (Clang/GCC) implement virtual inheritance with a vtable and how this is used during runtime?

BTW, I also saw this question: vtable in case of virtual inheritance, but it only points to answers related to virtual functions, which is not my question.

like image 740
Klaus Avatar asked Aug 13 '19 17:08

Klaus


3 Answers

The complete class inheritance hierarchy is already known in compile time.

True enough; so if the compiler knows the type of a most derived object, then it knows the offset of every subobject within that object. For such a purpose, a vtable is not needed.

For example, if B and C both virtually derive from A, and D derives from both B and C, then in the following code:

D d;
A* a = &d;

the conversion from D* to A* is, at most, adding a static offset to the address.

However, now consider this situation:

A* f(B* b) { return b; }
A* g(C* c) { return c; }

Here, f must be able to accept a pointer to any B object, including a B object that may be a subobject of a D object or of some other most derived class object. When compiling f, the compiler doesn't know the full set of derived classes of B.

If the B object is a most derived object, then the A subobject will be located at a certain offset. But what if the B object is part of a D object? The D object only contains one A object and it can't be located at its usual offsets from both the B and C subobjects. So the compiler has to pick a location for the A subobject of D, and then it has to provide a mechanism so that some code with a B* or C* can find out where the A subobject is. This depends solely on the inheritance hierarchy of the most derived type---so a vptr/vtable is an appropriate mechanism.

like image 89
Brian Bi Avatar answered Sep 24 '22 18:09

Brian Bi


The complete class inheritance hierarchy is already known at compiler time. But all the vptr related operations, such as to get the offsets to virtual base class and issue the virtual function call, are delayed until runtime, because only at runtime can we know the actual type of the object.

For example,

class A() { virtual bool a() { return false; } };
class B() : public virtual A { int a() { return 0; } };
B* ptr = new B();

// assuming function a()'s index is 2 at virtual function table
// the call
ptr->a();

// will be transformed by the compiler to (*ptr->vptr[2])(ptr)
// so a right call to a() will be issued according to the type of the object ptr points to
like image 35
Silentroar Avatar answered Sep 24 '22 18:09

Silentroar


However this offset can in the general case only be known at runtime,...

I can't get the point, what is runtime related here. The complete class inheritance hierarchy is already known in compile time.

The linked article at Wikipedia provides a good explanation with examples, I think.

The example code from that article:

struct Animal {
  virtual ~Animal() = default;
  virtual void Eat() {}
};

// Two classes virtually inheriting Animal:
struct Mammal : virtual Animal {
  virtual void Breathe() {}
};

struct WingedAnimal : virtual Animal {
  virtual void Flap() {}
};

// A bat is still a winged mammal
struct Bat : Mammal, WingedAnimal {
};

When you careate an object of type Bat, there are various ways a compiler may choose the object layout.

Option 1

+--------------+
| Animal       |
+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| WingedAnimal |
+--------------+
| vpointer     |
| Bat          |
+--------------+

Option 2

+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| WingedAnimal |
+--------------+
| vpointer     |
| Bat          |
+--------------+
| Animal       |
+--------------+

The values contained in vpointer in Mammal and WingedAnimal define the offsets to the Animal sub-object. Those values cannot be known until run time because the constructor of Mammal cannot know whether the subject is Bat or some other object. If the sub-object is Monkey, it won't derive from WingedAnimal. It will be just

struct Monkey : Mammal {
};

in which case, the object layout could be:

+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| Monkey       |
+--------------+
| Animal       |
+--------------+

As can be seen, the offset from the Mammal sub-object to the Animal sub-object is defined by the classes derived from Mammal. Hence, it can be defined only at runtime.

like image 27
R Sahu Avatar answered Sep 22 '22 18:09

R Sahu