14

I understand the basics of C++ virtual inheritance. However, I'm confused about where exactly I need to use the virtual keyword with a complex class hierarchy. For example, suppose I have the following classes:

            A
           / \
          B   C
         / \ / \
        D   E   F
         \ / \ /
          G   H
           \ /
            I

If I want to ensure that none of the classes appear more than once in any of the subclasses, which base classes need to be marked virtual? All of them? Or is it sufficient to use it only on those classes that derive directly from a class that may otherwise have multiple instances (i.e. B, C, D, E and F; and G and H (but only with the base class E, not with the base classes D and F))?

jchl
  • 6,332
  • 4
  • 27
  • 51

7 Answers7

26

I toyed a program together which could help you to study the intricacies of virtual bases. It prints the class hierarchy under I as a digraph suitable for graphiviz ( http://www.graphviz.org/ ). There's a counter for each instance which helps you to understand the construction order as well. Here's the programm:

#include <stdio.h>
int counter=0; 



#define CONN2(N,X,Y)\
    int id; N() { id=counter++; }\
    void conn() \
    {\
        printf("%s_%d->%s_%d\n",#N,this->id,#X,((X*)this)->id); \
        printf("%s_%d->%s_%d\n",#N,this->id,#Y,((Y*)this)->id); \
        X::conn(); \
        Y::conn();\
    }
#define CONN1(N,X)\
    int id; N() { id=counter++; }\
    void conn() \
    {\
        printf("%s_%d->%s_%d\n",#N,this->id,#X,((X*)this)->id); \
        X::conn(); \
    }

struct A { int id; A() { id=counter++; } void conn() {} };
struct B : A { CONN1(B,A) };
struct C : A { CONN1(C,A)  };
struct D : B { CONN1(D,B) };
struct E : B,C { CONN2(E,B,C) };
struct F : C { CONN1(F,C) };
struct G : D,E { CONN2(G,D,E) };
struct H : E,F { CONN2(H,E,F) };
struct I : G,H { CONN2(I,G,H) };
int main()
{
    printf("digraph inh {\n");
    I i; 
    i.conn(); 
    printf("}\n");
}

If I run this (g++ base.cc ; ./a.out >h.dot ; dot -Tpng -o o.png h.dot ; display o.png), I get the typical non-virtual base tree: alt text

Adding enough virtualness...

struct B : virtual A { CONN1(B,A) };
struct C : virtual A { CONN1(C,A)  };
struct D : virtual B { CONN1(D,B) };
struct E : virtual B, virtual C { CONN2(E,B,C) };
struct F : virtual C { CONN1(F,C) };
struct G : D, virtual E { CONN2(G,D,E) };
struct H : virtual E,F { CONN2(H,E,F) };
struct I : G,H { CONN2(I,G,H) };

..results in the diamond shape (look at the numbers to learn the construction order!!)

alt text

But if you make all bases virtual:

struct A { int id; A() { id=counter++; } void conn() {} };
struct B : virtual A { CONN1(B,A) };
struct C : virtual A { CONN1(C,A)  };
struct D : virtual B { CONN1(D,B) };
struct E : virtual B, virtual C { CONN2(E,B,C) };
struct F : virtual C { CONN1(F,C) };
struct G : virtual D, virtual E { CONN2(G,D,E) };
struct H : virtual E, virtual F { CONN2(H,E,F) };
struct I : virtual G,virtual H { CONN2(I,G,H) };

You get a diamond with a different initialization order:

alt text

Have fun!

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Nordic Mainframe
  • 28,058
  • 10
  • 66
  • 83
8

You have to specify virtual inheritance when inheriting from any of A, B, C, and E classes (that are at the top of a diamond).

class A;
class B: virtual A;
class C: virtual A;
class D: virtual B;
class E: virtual B, virtual C;
class F: virtual C;
class G:         D, virtual E;
class H: virtual E,         F;
class I:         G,         H;
Didier Trosset
  • 36,376
  • 13
  • 83
  • 122
  • Thanks, that makes sense now. As a follow-on question, can you explain what (if anything) would be the effect of using virtual inheritance everwhere? Nothing? Or would I end up with a different (larger) layout for instances of I? – jchl Aug 05 '10 at 12:47
  • 1
    Using virtual inheritance everywhere slows things down, a bit (I have never measured though). The main drawback of virtual inheritance IMHO, is that once you've cast your object into a pointer or reference to one of its virtual base, you cannot cast it back to the inheriting class. – Didier Trosset Aug 06 '10 at 08:00
  • I didn't realize that you couldn't downcast a pointer to a virtual base class. Thanks for pointing (no pun intended) that out. – jchl Aug 06 '10 at 08:30
  • Err, just to be sure there's no misunderstanding. You can downcast a `D*` to an `A*`. But then, you cannot *upcast* back the `A*` to a `D*`. – Didier Trosset Aug 06 '10 at 09:27
  • "_what (if anything) would be the effect of using virtual inheritance everywhere?_" Then the constructors of `I` get to call the constructors for all base classes. – curiousguy Nov 01 '11 at 01:52
  • @curiousguy: `dynamic_cast` cannot work because when it has an `A*`, it **cannot** know if this `A` is a virual part of a `B`, an `E`, an `F`, or any other type that inherits virtually from `A` – Didier Trosset Nov 02 '11 at 12:44
  • @DidierTrosset Sorry, I don't understand: why not? – curiousguy Nov 02 '11 at 18:08
  • @curiousguy: It is because of the way virtual inheritance *have to be* implemented. This information is simply lost. – Didier Trosset Nov 03 '11 at 08:49
  • 1
    @curiousguy Didier is possibly thinking about `static_cast`, not `dinamic_cast`. – mlvljr Jan 24 '12 at 09:38
2

My personal suggestion would be to start at B and C : virtual A, and then keep adding until the compiler stops complaining.

In reality, I'd say that B and C : virtual A, G and H : virtual E, and E : virtual B and C. All the other inheritance links can be normal inheritance. This monstrosity would take like six decades to make a virtual call, though.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • You also have to `F: virtual C` and `D: virtual B`, otherwise H would inherit virtualy of C through E, and non-virtualy from F. – Didier Trosset Aug 05 '10 at 12:37
  • Can you explain why only those need to be virtual? Why not D : virtual B and F : virtual C? And why would virtual function calls be slow on such a class? – jchl Aug 05 '10 at 12:39
  • You're right, those probably need to be virtual too. You really need to check it with your compiler. As for slow, because there's six dozen different types the compiler needs to check for each virtual function call. – Puppy Aug 05 '10 at 13:18
1

If you want to make sure that an object of the top class in the hierarchy (I in your case) contains exactly one subobject of each parent class, you have to find all classes in your hierarchy that have more than one superclass and make these classes virtual bases of their superclasses. That's it.

In your case classes A, B, C and E have to become virtual base classes every time you inherit from them in this hierarchy.

Classes D, F, G and H don't have to become virtual base classes.

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
0

On thing to keep in mind is C++ keeps a table of the inheritance. The more you add virtual classes, the longer will be compilation time (linkage) and the heavier would be the runtime.

In general, if one can avoid virtual class, you can replace by some templates or try to decouple in some way.

  • What do you mean by "table of the inheritance"? – curiousguy Oct 29 '15 at 17:54
  • C++ keeps a repository of the class hiearchy for dynamic binding. The more complex is the tree repository, the higher you get time for compilation and execution.. –  Oct 31 '15 at 10:09
0

If you want to have only one "physical" instance of each type for each instance of each type (only one A, only one B etc.) You'll just have to use virtual inheritance each time you use inheritance.

If you want separate instances of one of the types, use normal inheritance.

Klaim
  • 67,274
  • 36
  • 133
  • 188
  • The question is whether or not *all* the inheritance needs to be virtual to give the required heirarchy. The answer is no. – Mike Seymour Aug 05 '10 at 13:59
0

Edited: I thought A was the most derived class ;)

@Luther's answer really cool, but back to the original question:

You NEED to use virtual inheritance when inheriting from any class from which at least one other class inherits in the inheritance hierarchy (in Luther's diagrams it means at least two arrows point to the class).

Here it's unnecessary before D, F, G and H because only one class derives from them (and none derives from I at the moment).

However, if you don't know beforehand whether or not another class will inherit from your base class, you can add in virtual as a precaution. For example it's recommended for an Exception class to inherit virtually from std::exception by no other than Stroustrup himself.

As Luther has noted, it modifies the instantiation order (and has a slight effect on performances) but I would consider any design relying on the construction order to be wrong to begin with. And just as a precision: you still have the guarantees that base classes are initialized before any attribute of the derived class, and therefore before the execution of the derived's constructor body.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • My original question was unclear: class I (not A) is the most-derived class. So I think you mean (as others have said) that virtual is unnecessary before D, F, G and H (and I, though no-one derives from I). I don't care about the instantiation order; I _do_ care about object size. I'd like to avoid having unnecessary additional vtable pointers in each instance. – jchl Aug 05 '10 at 17:02
  • @jchl: Your have to keep in mind that virtual inheritance is normally implemented by embedding derived-to-base pointers into the object for each virtual derived-to-base link. In other words, you will not be really be saving much in terms of hidden household data by switching to virtual inheritance. On the contrary, with virtual inheritance the inner workings usually become a lot more complicated. – AnT stands with Russia Aug 05 '10 at 18:21
  • 1
    @AnT Internal pointers are just a possible implementation, and a small optimisation. They aren't needed as the vtable contains all the information. – curiousguy Aug 06 '15 at 04:56