Why Can't Static_Cast Be Used to Down-Cast When Virtual Inheritance Is Involved

Why can't static_cast be used to down-cast when virtual inheritance is involved?

The technical problem is that there's no way to work out from a Base* what the offset is between the start of the Base sub-object and the start of the Derived object.

In your example it appears OK, because there's only one class in sight with a Base base, and so it appears irrelevant that the inheritance is virtual. But the compiler doesn't know whether someone defined another class Derived2 : public virtual Base, public Derived {}, and is casting a Base* pointing at the Base subobject of that. In general[*], the offset between the Base subobject and the Derived subobject within Derived2 might not be the same as the offset between the Base subobject and the complete Derived object of an object whose most-derived type is Derived, precisely because Base is virtually inherited.

So there's no way to know the dynamic type of the complete object, and different offsets between the pointer you've given the cast, and the required result, depending what that dynamic type is. Hence the cast is impossible.

Your Base has no virtual functions and hence no RTTI, so there certainly is no way to tell the type of the complete object. The cast is still banned even if Base does have RTTI (I don't immediately know why), but I guess without checking that a dynamic_cast is possible in that case.

[*] by which I mean, if this example doesn't prove the point then keep adding more virtual inheritance until you find a case where the offsets are different ;-)

How can static_cast be used with virtual inheritance?

When you have a pointer to a Derived in your case, it is clear which Base is to be used and you can even implicitly convert the pointer to Derived to a pointer to Base! If any address adjustment is needed the compiler will figure out how to do so using embedded pointer, a vtable, or whatever: the exact approach isn't prescribed by the C++ standard. What is done exact depends on the ABI. For example, for the Itanium C++ ABI it seems the offsets for virtual bases are store in the virtual tables.

Why static upcast with virtual inheritance is always correct for GCC?

Not sure, but it seems like you confuse upcasting with downcasting. In your code there are only upcasts, and those are fine. You get the expected compiler error for example with this:

  D obj;
A* a4 = static_cast<A*>(&obj);
D* d = static_cast<D*>(a4);

gcc reports:

<source>: In function 'int main()':
<source>:26:28: error: cannot convert from pointer to base class 'A' to pointer to derived class 'D' because the base is virtual
26 | D* d = static_cast<D*>(a4);
| ^

If you remove the virtual inheritance in your example the static_cast would fail as well, because then the cast is ambiguous.

Static cast - Cannot cast through virtual inheritance

class A {virtual ~A {}}
class B {virtual ~B {}}
class C : A, B {}

C o;
// Now, some implicit conversions:
A& a = c;
B& b = c;
// static casts back:
C& ca = static_cast<C>(a);
C& cb = static_cast<C>(b);
// dynamic casts over the type:
A& ab = dynamic_cast<A>(b);
B& ba = dynamic_cast<B>(a);

The dynamic cast can cast accross the hierarchy (or checked to derived types), while the static casts can only cast up and down the hierarchy, where statically proovable sound, aside from the object actually being of the target type.

Where possible, compilers reduce a dynamic cast to a static one for efficiency.

C++ cannot convert from base A to derived type B via virtual base A

In order to understand the cast system, you need to dive into the object model.

The classic representation of a simple hierarchy model is containment: if B derives from A then the B object will, in fact, contain an A subobject alongside its own attributes.

With this model downcasting is a simple pointer manipulation by an offset known at compilation time, which depends on the memory layout of B.

This is what static_cast does: a static cast is dubbed static because the computation of what is necessary for the cast is done at compile-time, be it pointer arithmetic or conversions (*).

However, when virtual inheritance kicks in, things tend to become a bit more difficult. The main issue is that with virtual inheritance all subclasses share the same instance of the subobject. In order to do that, B will have a pointer to an A, instead of an A proper, and the A base class object will be instantiated outside of B.

Therefore, it's impossible at compilation time to be able to deduce the necessary pointer arithmetic: it depends on the runtime type of the object.

Whenever there is a runtime type dependency, you need RTTI (RunTime Type Information), and making use of RTTI for casts is the job of dynamic_cast.

In summary:

  • compile-time downcast: static_cast
  • run-time downcast: dynamic_cast

The other two are also compile-time casts, but they are so specific that it's easy to remember what they are for... and they are smelly, so better not use them at all anyway.

(*) As noted by @curiousguy in the comments, this only holds for downcasting. A static_cast allows upcasting regardless of virtual or simple inheritance, though then the cast is also unnecessary.

Downcasting in C++ with virtual base class

Is this simply impossible to do?

Yes.

How does the C++ standard (11 or 14) state that this is impossible?

It is stated under the requirements of static_cast.

[expr.static.cast] - emphasis mine

11 A prvalue of type “pointer to cv1 B,” where B is a class type,
can be converted to a prvalue of type “pointer to cv2 D,” where D is a
class derived (Clause [class.derived]) from B, if a valid standard
conversion from “pointer to D” to “pointer to B” exists ([conv.ptr]),
cv2 is the same cv-qualification as, or greater cv-qualification than,
cv1, and B is neither a virtual base class of D nor a base class of
a virtual base class of D
.

As you can see, Base cannot be virtual or even a non-virtual base of another virtual base.

What is the underlying implementation of a standard compiler like gcc which makes this feature impossible?

Virtual inheritance is a mechanism that allows the same Base subobject to be shared between different intermediate base classes. For instance, if you were to add:

class Derived2: public virtual Base {};
class MostDerived: public Derived, public Derived2 {};

Then MostDerived will have a sub-object of type Derived and type Derived2, but unlike regular inheritance, they won't each have their own Base sub-object (thus 2 Base sub-objects in MostDerived). Instead there will be only one Base sub-object, and both Derived and Derived2 will refer to it.

This is commonly accomplished by Derived and Derived2 accessing the Base indirectly through some pointer. They cannot rely on it being placed inside themselves at a certain offset. The location of their Base is determined by the most derived object. So if you have a Base*, there is no one well-determined way to go to the Derived object that refers to it.

In the case of non-virtual inheritance, the Base would be located at a place known to the compiler relative to the Derived sub-object that contains it fully. So the compiler could do the pointer arithmetic required to obtain the Derived sub-object. But in your case, it cannot, because there is no guarantee of relative placement. The access is (generally) indirect.


You didn't ask, but I might as well address that too. The reason a dynamic_cast works for polymorphic classes, is that polymorphic classes can have RTTI associated with them in their vtable (if they have one). And because that Base* will point to the vtable of MostDerived (or Derived), the dynamic_cast mechanism can leverage information written in that table to figure out how to obtain the sub-object it needs. The table can contain all the offset information it needs to get from any one sub-object to any other sub-object. And because it is the table of the most derived object (at run-time), that information is fixed and reliable.

When is static cast safe when you are using multiple inheritance?

A cross-cast:

struct Base1 { virtual void f1(); };
struct Base2 { virtual void f2(); };
struct Derived : Base1, Base2 {};

Base1* b1 = new Derived();
Base2* b2 = dynamic_cast<Base2*>(b1);

requires use of dynamic_cast, it cannot be done with static_cast (static_cast should have caused a compile-time error). dynamic_cast will also fail if either base class is not polymorphic (the presence of virtual functions is NOT optional).

See this explanation on MSDN

Can you static_cast this to a derived class in a base class constructor then use the result later?

In my opinion this is well-defined according to the current wording of the standard: the C object exists at the time of the static_cast, although it is under construction and its lifetime has not yet begun. This would seem to make the static_cast well-defined according to [expr.static.cast]/11, which reads in part:

... If the prvalue of type “pointer to cv1 B” points to a B that is actually a base class subobject of an object of type D, the resulting pointer points to the enclosing object of type D. Otherwise, the behavior is undefined.

It doesn't say that the D object's lifetime must have begun.

We might also want to look at the explicit rule about when it becomes legal to perform an implicit conversion from derived to base, [class.cdtor]/3:

To explicitly or implicitly convert a pointer (a glvalue) referring to an object of class X to a pointer (reference) to a direct or indirect base class B of X, the construction of X and the construction of all of its direct or indirect bases that directly or indirectly derive from B shall have started and the destruction of these classes shall not have completed, otherwise the conversion results in undefined behavior. To form a pointer to (or access the value of) a direct non-static member of an object obj, the construction of obj shall have started and its destruction shall not have completed, otherwise the computation of the pointer value (or accessing the member value) results in undefined behavior.

According to this rule, as soon as the compiler starts constructing the base class A<C>, it is well-defined to implicitly convert from C* to A<C>*. Before that point, it results in UB. The reason for this, basically, has to do with virtual base classes: if the path by which A<C> is inherited by C contains any virtual inheritance, the conversion may rely on data that are set up by one of the constructors in the chain. For a conversion from base to derived, if there is indeed any virtual inheritance on the chain, static_cast will not compile, so we don't really need to ask ourselves the question, but are those data sufficient for going the other way?

I really can't see anything in the text of the standard, nor any rationale, for the static_cast in your example not being well-defined, nor in any other case of static_casting from base to derived when the reverse implicit conversion (or static_cast) would be allowed (excepting the case of virtual inheritance, which as I said before, leads to a compile error anyway).

(Would it be well-defined to do it even earlier? In most cases this won't be possible; how could you possibly attempt to static_cast from B* to D* before the conversion from D* to B* is allowed, without having obtained the B* pointer precisely by doing the latter? If the answer is that you got from D* to B* through an intermediate base class C1 whose constructor has started, but there is another intermediate base class C2 sharing the same B base class subobject and its construction hasn't started yet, then B is a virtual base class, and again, this means the compiler will stop you from then trying to static_cast from B* back down to D*. So I think there are no issues left to resolve here.)

How to downcast from non-polymorphic virtual base class?

There is an implicit unambigious conversion from MostDerived& to its ViBase&. A static_cast can express such a conversion explicitly, and can also do the opposite conversion. That’s the kinds of conversions that static_cast does.

As the OP noted a static_cast down from virtual base is invalid.

The source code below illustrates why:

#include <iostream>
using namespace std;

struct B { virtual ~B(){} };
struct D: virtual B {};
struct E: virtual B {};
struct X: D, E {};

auto main() -> int
{
X x;
B& b = static_cast<E&>( x );

// Can't do the following for the address adjustment that would work for
// D sub-object won't work for E sub-object, yet declarations of D and E
// are identical -- so the address adjustment can't be inferred from that.
//
//static_cast<D&>( b );

// This is OK:
dynamic_cast<D&>( b );
}

Essentially, as this shows, you can't infer the address adjustment from the declaration of D (or E) alone. And neither can the compiler. This also rules out reinterpret_cast.



Related Topics



Leave a reply



Submit