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.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Klaus
  • 24,205
  • 7
  • 58
  • 113
  • 4
    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. – Jesper Juhl Aug 13 '19 at 17:52
  • 1
    @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. – Klaus Aug 13 '19 at 17:52
  • 1
    Thanks for pointing to an unrelated answer an mark as duplicate. The question is about "virtual inheritance" and not "virtual function! – Klaus Aug 13 '19 at 17:55
  • 2
    @Klaus People make mistakes or get confused sometimes. Please remember to stay civil and show patience with your comments and edits. – François Andrieux Aug 13 '19 at 17:57
  • 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. – user4581301 Aug 13 '19 at 17:57
  • 1
    @user4581301 The question is fine and clear. Sometimes people just happened to have asked or seen something similar, so jump the gun on closing. – François Andrieux Aug 13 '19 at 17:59
  • 1
    @FrançoisAndrieux: Agree! But it sometimes quite hard to get an question reopened again. I noted explicitly that it is not about "virtual functions"... – Klaus Aug 13 '19 at 18:00
  • If that helps (or adds more confusion), diamond problem gets double size of pointer as class size on both gcc and clang: https://wandbox.org/permlink/XIQzLBjhYJijc5G3 – Yksisarvinen Aug 13 '19 at 18:04
  • Looks like this answer might has the piece you are missing: https://stackoverflow.com/a/10905259/4342498 – NathanOliver Aug 13 '19 at 18:09
  • 1
    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. – curiousguy Aug 16 '19 at 22:16

3 Answers3

17

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.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • 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! – Klaus Aug 13 '19 at 18:15
  • 2
    @Klaus 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. – Miles Budnek Aug 13 '19 at 19:02
  • @MilesBudnek: I will go and think again :-) Thanks! – Klaus Aug 13 '19 at 19:40
  • "_at most, adding a static offset to the address_" It's slightly more: you need to check for null first. – curiousguy Aug 19 '19 at 05:40
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.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
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
Silentroar
  • 71
  • 1
  • 9
  • 2
    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. – Maëlan Apr 17 '21 at 17:08
  • 1
    @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 comment – Silentroar Apr 18 '21 at 08:00
  • 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. – Maëlan Apr 18 '21 at 12:50
  • 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 `A` – Silentroar Apr 19 '21 at 03:14
  • [This post is useful for understanding the mechanics of upcasting, e.g. `A* a = &d`](https://stackoverflow.com/questions/26751035/how-do-upcasting-and-vtables-work-together-to-ensure-correct-dynamic-binding) – Silentroar Apr 19 '21 at 03:18
  • @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? – curiousguy May 04 '21 at 00:00
  • @curiousguy 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](https://stackoverflow.com/questions/44618230), [2](https://reverseengineering.stackexchange.com/questions/12857) and so on). – Maëlan May 04 '21 at 15:51
  • However [this article](https://learn.microsoft.com/en-us/archive/msdn-magazine/2000/march/c-q-a-atl-virtual-functions-and-vtables) 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? – Maëlan May 04 '21 at 15:54