Virtual inheritance is the C++ feature that fixes the diamond problem. By marking a base class as virtual in the inheritance specifier, the compiler shares one copy of that base across the entire hierarchy instead of giving every intermediate derived class its own duplicate. This lesson covers the syntax, what virtual actually means at the inheritance specifier position, the constructor rule that comes with it, the memory layout cost, and when it's worth using.
A quick recap of the diamond. When a class D derives from two classes B and C that both derive from a common base A, plain (non-virtual) multiple inheritance gives D two separate A subobjects, one inherited through B and one through C. Any data field declared in A exists twice inside every D object, and any unqualified access to an A member from D is ambiguous because the compiler can't tell which copy is meant. Scope resolution like bundle.PhysicalItem::id and bundle.DigitalItem::id becomes necessary everywhere, and changes to one copy don't affect the other. That's the problem virtual inheritance solves.
The diagram shows the layout that triggers the problem: two parallel BillableItem subobjects, one reachable through each path. Virtual inheritance collapses both copies into one. Everything in this lesson is built around showing how that collapse works and what it costs.
The fix is a single keyword in the right place. When B and C derive from A, both put virtual in front of public (or private, or protected) in their inheritance specifier:
The virtual keyword belongs on the inheritance specifier of every class that should share the base, not on D. D doesn't need to do anything special at the inheritance line. The compiler sees that both B and C virtually inherit from A and arranges for D to contain exactly one A subobject that both B and C see.
This use of virtual is a different feature from virtual member functions. Same keyword, different mechanism. Virtual functions enable runtime dispatch through a vtable; virtual inheritance enables a single shared base subobject through a virtual base table. They sit in the same family (both add a layer of indirection) but they solve different problems and you can use either one without the other.
The keyword can be combined with any access specifier: virtual public A, virtual protected A, and virtual private A are all valid. The order of virtual and the access specifier doesn't matter to the compiler; both virtual public A and public virtual A mean the same thing. The conventional ordering most codebases follow is virtual first, then the access specifier, but both forms appear in real code and in the standard.
One more detail: the virtual keyword has to appear on the inheritance specifier of every intermediate class that should share the base. If only B declares virtual public A but C declares plain public A, the result is a hybrid: C's A subobject is separate, while B shares its A with anyone else who virtually inherits. That's almost never the intended outcome. The rule of thumb is that all intermediates in a diamond should agree to virtually inherit from the shared base, or none should.
A minimal sketch of the syntax, before the full e-commerce example.
The virtual keyword sits on PhysicalItem and DigitalItem, the classes that directly derive from BillableItem. BundledProduct derives from PhysicalItem and DigitalItem, not from BillableItem directly, so its inheritance line stays plain. The compiler does the rest of the work to make sure the single shared BillableItem ends up where it needs to be.
virtual Means HereWithout virtual inheritance, every path from D up to A produces its own A subobject. With virtual inheritance, the compiler collapses all of those paths into one shared A subobject that lives directly in the most-derived object, D. B and C no longer "own" a copy of A; they each hold a reference (typically a pointer or offset) to the single shared A subobject that D provides.
Concretely:
sizeof(D) no longer counts A's fields twice. There's one A worth of data, plus whatever B and C add, plus the per-object indirection that virtual inheritance introduces.A's members from a D object is no longer ambiguous, because there's only one copy to refer to. bundle.id resolves cleanly without any scope resolution.A's state through the B path is visible through the C path, because they're the same subobject.That last point is worth pausing on. With non-virtual diamond inheritance, writing to bundle.PhysicalItem::id = 42 leaves bundle.DigitalItem::id untouched, because they're separate fields that just happen to share a name. With virtual inheritance, bundle.id = 42 writes once and every path through the hierarchy reads the same 42 back. That's typically the desired behavior, and it's the reason virtual inheritance exists.
The same shared-identity property applies to functions inherited from A. If BillableItem defines a display() method, calling bundle.display() is no longer ambiguous, because there's exactly one display to dispatch to. Casting bundle to a PhysicalItem& and calling display() on that, then casting to a DigitalItem& and calling display() again, both run the same method on the same shared BillableItem subobject. They print the same id, name, and price, because there's only one of each.
Take the diamond set up with a billable item shared between physical and digital products in a bundle, this time with virtual inheritance applied. The base BillableItem carries the fields every billable thing has: an identifier, a name, and a price. PhysicalItem and DigitalItem virtually inherit from it, and BundledProduct derives from both.
The interesting thing is what's not in the output. BillableItem is constructed exactly once, even though BundledProduct derives from two classes that both inherit from it. With non-virtual inheritance, the output would contain two BillableItem(1001) constructed lines and the line bundle.id would be a compile error because of ambiguity. Here, there's one shared base subobject, and bundle.id resolves cleanly to that single id.
The other interesting thing is the BundledProduct constructor's initializer list. It calls BillableItem(i, n, p) directly, even though BundledProduct doesn't list BillableItem in its inheritance line. That's the most-derived constructor rule.
Compare what happens with the virtual keyword removed from PhysicalItem and DigitalItem's inheritance lines (turning the hierarchy into a non-virtual diamond): the program produces two BillableItem(1001) constructed lines, and the line std::cout << "id: " << bundle.id fails to compile with "request for member 'id' is ambiguous". bundle.PhysicalItem::id becomes necessary to pick a path explicitly, and two independent id fields live inside the one bundle. Adding virtual back collapses both copies into a single shared one and resolves the ambiguity in one stroke.
This is the part that catches everyone off guard the first time. With virtual inheritance, the most-derived class (the one being constructed, BundledProduct here) is responsible for invoking the virtual base's constructor directly. The intermediate classes' attempts to construct the virtual base are ignored.
Look at the constructors again. PhysicalItem's initializer list says : BillableItem(i, n, p). So does DigitalItem's. If both of those calls actually ran, BillableItem would be constructed twice, which is exactly the problem we set out to avoid. The rule is: when the object being created is a BundledProduct, the BillableItem(i, n, p) calls inside PhysicalItem and DigitalItem are ignored, and only the BillableItem(i, n, p) call inside BundledProduct's own initializer list runs.
When the object being created is just a PhysicalItem standing on its own (not as part of a BundledProduct), PhysicalItem's call to BillableItem(i, n, p) does run, because in that case PhysicalItem is the most-derived class.
The rule, stated cleanly:
The reason behind the rule is consistency. There's only one shared BillableItem subobject. Only one constructor call should set it up. The compiler picks the most-derived class for that responsibility, because that's the only class that knows what kind of object is actually being built and what the right arguments are.
This has a sharp consequence. If BundledProduct does not list BillableItem in its initializer list, the compiler tries to call BillableItem's default constructor. If BillableItem has no default constructor, the result is a compile error.
The error message from g++ looks roughly like this:
The compiler is telling exactly what happened. Because BundledProduct is the most-derived class and didn't initialize the virtual base explicitly, the compiler tried to fall back to the default constructor of BillableItem. There isn't one, so the compile fails. The fix is to add BillableItem(i, n, p) to BundledProduct's initializer list, exactly the way the working version above does.
There's a related consequence worth knowing. The initializer-list calls in PhysicalItem and DigitalItem aren't removed by the compiler; they're just skipped when those classes are not the most-derived. There's no need to delete them. They keep the intermediate classes usable as standalone types (a PhysicalItem constructed by itself still needs to initialize its base), and they're harmlessly ignored when the intermediate class is part of a bigger BundledProduct.
The same rule applies recursively. In a deeper hierarchy where BundledProduct itself becomes the base of a GiftWrappedBundle, GiftWrappedBundle is now the most-derived class and has to initialize BillableItem itself. The BillableItem(i, n, p) call inside BundledProduct gets skipped in that case, the same way the calls inside PhysicalItem and DigitalItem get skipped when BundledProduct is the most-derived. Whichever class is at the top of the runtime object's actual type owns the responsibility, and every other call to the virtual base's constructor gets ignored.
This rule has a useful corollary: virtual-base initialization happens before any non-virtual base initialization, regardless of where the call appears in the most-derived constructor's initializer list. Even though BundledProduct writes BillableItem(i, n, p), PhysicalItem(i, n, p, w), DigitalItem(i, n, p, s) in that order, swapping them around (PhysicalItem(...), BillableItem(...), DigitalItem(...)) doesn't change the actual order of construction. The standard fixes the order at "virtual bases first, then non-virtual bases in declaration order"; the initializer list just supplies the arguments. Still, write them in the standard's order to keep the code readable; a misordered list is not a bug.
The order of construction with virtual inheritance is slightly different from the non-virtual case. The rule the standard uses:
For BundledProduct, this means BillableItem is constructed first (it's the only virtual base), then PhysicalItem (without re-constructing BillableItem), then DigitalItem (without re-constructing BillableItem), then the body of BundledProduct's constructor runs. The output earlier showed exactly this order.
Destruction is the reverse order: most-derived body, then DigitalItem, then PhysicalItem, then BillableItem last.
This matters because virtual bases get fully constructed before any non-virtual intermediate base. If PhysicalItem's constructor body assumes BillableItem's fields are already valid, it can rely on that, because BillableItem was constructed first. The same is true on the destruction side: when BillableItem's destructor runs, both PhysicalItem and DigitalItem have already been destroyed, so it can safely tear down anything they were sharing.
A quick way to see the order in action. The following hierarchy uses default constructors only, with print statements, so no argument-passing details get in the way.
The numbers tell the story. Construction goes top-down, virtual base first, and destruction goes strictly in reverse. The shared BillableItem is set up before anyone touches it and torn down after everyone else is gone.
To understand the cost of virtual inheritance, look at how the object is laid out. With non-virtual diamond inheritance, the layout duplicates the base:
Two complete BillableItem subobjects live inside one BundledProduct. The fields exist twice. With virtual inheritance, the layout collapses to a single shared BillableItem, and the intermediate classes hold an indirection to find it:
The exact layout is implementation-defined and varies between compilers (GCC, Clang, and MSVC each pick their own arrangement), but the shape is the same. The virtual base sits in one location, and the intermediate parts each hold some form of indirection (often called a virtual base pointer, or stored as an offset in a virtual base table) so they can find the shared base from any pointer or reference of their own type.
The reason for the indirection is that the same intermediate class can show up in many different most-derived layouts. A PhysicalItem standing on its own places BillableItem immediately below itself in memory. A PhysicalItem that's part of a BundledProduct places BillableItem further away, after DigitalItem's data. The PhysicalItem code is compiled once, and that one compiled version has to work in both layouts. The mechanism: PhysicalItem always asks "where is my virtual base?" through a pointer or offset that's filled in at construction time by whichever most-derived class is being built. The most-derived class knows the layout, so it knows the right offset to write into the virtual-base pointer.
What this means for sizeof:
| Layout | Size on a typical 64-bit g++ build |
|---|---|
Non-virtual diamond, two BillableItem copies | Roughly the size of two BillableItems plus the intermediate fields |
Virtual inheritance, one shared BillableItem | Roughly one BillableItem plus the intermediate fields plus one pointer-sized virtual base reference per intermediate class |
The duplicated BillableItem data is saved (which can be a lot if the base has big fields like a std::string), at the cost of one pointer-sized field per intermediate class plus a small bit of indirection on each access.
Every access through an intermediate class to a virtual base member, like physicalItem.id when physicalItem is a PhysicalItem& referring to a BundledProduct, goes through one extra pointer dereference to reach the shared base. The cost is one indirection, comparable to a virtual function call's vtable lookup. It's not free, but it's not catastrophic either. For most code, the correctness benefit dominates; for tight loops over virtual-base members, it can show up in a profiler.
The size impact is easy to verify:
BillableItem is 40 bytes (4 for id, 4 padding, 32 for std::string on libstdc++, 8 for price). PhysicalItem adds an 8-byte weightKg, an 8-byte virtual base pointer, and the 40-byte BillableItem, which lays out to 56 bytes total. The interesting one is BundledProduct: at 80 bytes, it's noticeably smaller than the 96+ bytes you'd get if BillableItem were duplicated. The exact numbers vary by compiler and standard library, but the pattern is consistent: virtual inheritance trades a small per-class indirection for not duplicating the base.
If the base were larger, the savings would grow. Consider a BillableItem that carries a 200-byte description string and a 100-byte category path. The non-virtual diamond would store both fields twice inside BundledProduct, costing roughly 600 bytes for BillableItem data alone. The virtual version would store one copy at 300 bytes plus two 8-byte virtual-base pointers, totaling 316 bytes for the same content. The bigger the shared base, the more virtual inheritance pays off in memory; the smaller and more frequently accessed it is, the more the per-access indirection cost matters.
Virtual inheritance solves a real problem, but it isn't free. The trade-offs:
| Aspect | Non-virtual inheritance | Virtual inheritance |
|---|---|---|
| Number of base subobjects in a diamond | One per path (duplicated) | One shared |
| Ambiguity on unqualified base member access | Yes, scope resolution required | No, single subobject |
| Per-object size | Smaller per intermediate class, but base data is duplicated | Larger per intermediate class, base data shared |
| Access cost to base members through intermediate classes | Direct field offset | One extra indirection |
| Constructor responsibility | Each intermediate class initializes its own copy | Most-derived class initializes the shared base |
| Compile-time complexity | Simpler | More involved (compiler manages the shared layout) |
A reasonable default: don't use virtual inheritance unless you have a real diamond and you genuinely need the shared base to be one object. If your design has multiple inheritance but no diamond, virtual inheritance buys you nothing. If you have a diamond but the duplicated base data is fine (the two paths really do represent independent state), non-virtual inheritance is simpler.
The cases where virtual inheritance is the right tool tend to share a few traits:
BillableItem example fits this. There's one billable thing; both the physical and the digital aspects are facets of the same item, not two separate items.For most application code, the simpler answer is to avoid the diamond entirely. Composition (one class holding another as a member) almost always replaces problematic multiple inheritance with something easier to reason about. A BundledProduct could just hold a BillableItem, a PhysicalAttributes, and a DigitalAttributes as members instead of inheriting from a tangle. You give up the "is-a" relationship in exchange for a layout you can describe in one sentence.
The composition version of the e-commerce example would look something like this:
Now bundle.billing.id is unambiguous by construction. There's no diamond, no virtual inheritance, no most-derived constructor rule, and no per-access indirection. The price you pay is that BundledProduct is no longer a PhysicalItem or a DigitalItem for the purpose of polymorphic code (you can't pass it to a function that takes PhysicalItem&), but in many designs that's fine, because most "is-a" relationships in application domains turn out to be "has-a" on closer inspection.
Where virtual inheritance does show up in real code is in standard library design (the iostream hierarchy uses it: iostream inherits virtually from both istream and ostream, which both virtually inherit from ios) and in older frameworks that bake multiple inheritance deeply into their interfaces. If you're working with such code, knowing how virtual inheritance behaves matters; if you're starting fresh, prefer composition.
The iostream case is worth a closer look because it illustrates exactly the kind of problem virtual inheritance is built for. std::ios carries the stream's state: error flags, formatting options (precision, width, fill character), and the underlying buffer. std::istream adds read operations on top of that state. std::ostream adds write operations on top of the same state. std::iostream provides both reading and writing, and it inherits from both istream and ostream. Without virtual inheritance, an iostream would have two ios subobjects: setting the precision through the read side wouldn't affect the precision used by the write side, and setting an error flag during a failed read wouldn't be visible to subsequent writes. That would make iostream unusable as a single coherent stream object. Virtual inheritance gives iostream exactly one shared ios, and the read and write operations agree on its state. This is why std::cin >> x and std::cout << y formatting work the way you'd expect when used through a combined stream like std::stringstream.
One subtle behavior change worth noticing: with virtual inheritance, casting from a derived type back down through a virtual base is more constrained than with non-virtual inheritance. static_cast from a BillableItem* to a BundledProduct* is illegal, because the compiler can't determine the offset of the most-derived object from a pointer to a virtual base. The virtual-base layout is per-most-derived-type, so the compiler genuinely doesn't have enough information to reverse the path at compile time.
dynamic_cast works because it consults the runtime type information attached to the object (which requires the virtual base to have at least one virtual function for RTTI to be available; in practice, a virtual destructor on the base is enough). It performs the offset calculation by looking at what the actual most-derived type is, not at compile-time information.
The takeaway: with virtual inheritance, downcasting from a virtual-base pointer is a runtime operation, not a compile-time one. If you find yourself needing to downcast often through a virtual base, that's usually a sign that the design is leaning on inheritance harder than it should, and a refactor toward composition or a different polymorphism strategy may be cleaner.
virtualC++ overloads the virtual keyword for two related but distinct purposes:
Use of virtual | Where it goes | What it does |
|---|---|---|
| Virtual function | In front of a member function declaration | Enables runtime dispatch through a vtable, so the right override is called based on the object's actual type |
| Virtual inheritance | In front of public/protected/private in an inheritance specifier | Causes the compiler to share one subobject of the base across the entire hierarchy, instead of duplicating it |
The two features can be used together, but neither requires the other. A class can have virtual functions without virtual inheritance, and a class can virtually inherit a base that has no virtual functions at all. Both add a small amount of per-object storage (a vtable pointer for virtual functions, a virtual-base pointer for virtual inheritance) and a small amount of indirection on each relevant access. Beyond that, they're independent mechanisms.
A small detail that catches some people out: when a class with virtual bases is copied or assigned, the compiler-synthesized copy constructor and copy assignment operator already do the right thing. They copy the shared virtual base exactly once, even though there's a path to it through every intermediate class. You don't have to write any special handling for the virtual-inheritance case.
If you write your own copy constructor for the most-derived class, you do need to explicitly initialize the virtual base in the initializer list, the same way you do for the regular constructor. Otherwise the compiler falls back to the virtual base's default constructor, which may not exist or may not produce the value you want. The same most-derived rule that governs construction governs copy construction.
The PhysicalItem(other) and DigitalItem(other) calls each invoke their own copy constructors, which would normally try to copy-construct BillableItem themselves. Because BillableItem is a virtual base, those attempts are skipped, and the explicit BillableItem(other) call from the most-derived class is the one that actually runs. The behavior mirrors the regular constructor exactly.
This is one of those areas where the rule "the most-derived class is responsible for the virtual base" is consistent and predictable across every special function: regular constructors, copy constructors, move constructors, and copy/move assignment operators all follow it. Once the rule is clear, the special functions stop being a separate set of corner cases and start being a single rule applied in different contexts.
This lesson covered the mechanics of fixing the diamond problem. The next pieces of the inheritance puzzle deal with making overrides safe and explicit (what the override keyword adds, why mistyped overrides are a real bug source) and what happens when a derived object is copied into a base-class slot (object slicing). Those each get their own chapter.
10 quizzes