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

3

11

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.

Electrophorus answered 13/8, 2019 at 17:47 Comment(10)
Note: vtable/vptr are impletation details. A compiler is not required to use them as long as they can somehow implement the behaviour required by the standard.Runthrough
@RadosławCybulski: You are wrong, so please follow the links I gave. The question explicitly shows that there is a vtable involved without using any virtual function.Electrophorus
Thanks for pointing to an unrelated answer an mark as duplicate. The question is about "virtual inheritance" and not "virtual function!Electrophorus
@Electrophorus People make mistakes or get confused sometimes. Please remember to stay civil and show patience with your comments and edits.Videlicet
Something about this question is really messing people, myself included, up. On read 2 it makes perfect sense and does NOT match the duplicate. I can't think of a thing to suggest changing. Other than a typo in the title. Fixed.Sextillion
@Sextillion The question is fine and clear. Sometimes people just happened to have asked or seen something similar, so jump the gun on closing.Videlicet
@FrançoisAndrieux: Agree! But it sometimes quite hard to get an question reopened again. I noted explicitly that it is not about "virtual functions"...Electrophorus
If that helps (or adds more confusion), diamond problem gets double size of pointer as class size on both gcc and clang: wandbox.org/permlink/XIQzLBjhYJijc5G3Hypogynous
Looks like this answer might has the piece you are missing: https://mcmap.net/q/1014169/-why-does-virtual-keyword-increase-the-size-of-derived-a-classKeyboard
Virtual bases and virtual functions are quite similar: they are relationships that can be overridden. The reason for virtual functions vtable entries and virtual bases vtable entries is exactly the same: to allow dynamic behavior based on only knowing the static type.Ayeshaayin
K
20

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.

Khalif answered 13/8, 2019 at 18:7 Comment(4)
Good point! A alternative "solution" may be to have conversion functions "somewhere" implemented "like" template instances for every seen conversion. Maybe more complicated and more code, but less object size. OK, having a single conversion function which takes the offset from the vtable is one, and a often used solution. Thanks!Electrophorus
@Electrophorus That solution falls apart in different situations. Consider B* b = (rand() % 2 == 0) ? new B : new D; f(b); It is impossible for the compiler to know, at compile time the correct offset to use to find b's A subobject in every situation.Selfpropelled
@MilesBudnek: I will go and think again :-) Thanks!Electrophorus
"at most, adding a static offset to the address" It's slightly more: you need to check for null first.Ayeshaayin
C
5

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.

Callida answered 13/8, 2019 at 18:13 Comment(0)
L
1

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
Lyophilic answered 17/4, 2021 at 11:19 Comment(8)
That is very true and worth mentioning. Unfortunately, the OP has been quite vocal in ruling out answers about virtual functions. In fact, virtual inheritance requires a vtable even when there is no virtual function, as other answers have outlined. The essential reason is the same: the layout of a class depends on whether it is part of a descendant class, hence is dynamic.Thurston
@Maëlan I agree with you. The accepted answer states, "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", which is misleading, because the compiler does not know or care about the type of the object pointed by a. It only checks the pointer's static type, i.e. A, then transforms vptr related operations. Unfortunately I don't have enough reputation to commentLyophilic
Here, take some. :-) My understanding of the said answer is that a decent C++ compiler would optimize operations so as to bypass the vtable when it knows statically the dynamic type of an object, as in the given example D d; A* a = &d;. So it does care about tracking the dynamic type when possible, for optimization purposes, although of course this is not feasible in the general case.Thurston
I'll clarify myself. The compiler indeed needs to know the type of d to perform the correct upcasting A* a = &d. But I don't think this relates to compiler optimizations. And after the upcasting is done, the compiler generated code does not care about what a actually points to, i.e. its dynamic type, because a is treated as if it points to an object of type ALyophilic
This post is useful for understanding the mechanics of upcasting, e.g. A* a = &dLyophilic
@Maëlan "virtual inheritance requires a vtable even when there is no virtual function, as other answers have outlined" How is a real vtable needed? Does the Microsoft official ABI use one?Ayeshaayin
@Ayeshaayin TBH I am not a C++ person myself, I only read these answers and they made sense to me. Now I am fairly convinced that for the general case you do need the piece of information that a vtable delivers, even though you can optimize it away in special cases. Anything dynamic is undecidable at compile-time, that’s why it is dynamic. About the Microsoft ABI, I don’t know, all I can tell is that Google search results imply that it does (1, 2 and so on).Thurston
However this article mentions __declspec(novtable), “a Microsoft-specific optimization hint that tells the compiler: this class is never used by itself, but only as a base class for other classes, so don't bother with all that vtable stuff, thank you.” Maybe that was what you were thinking of?Thurston

© 2022 - 2024 — McMap. All rights reserved.