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

20,461

Solution 1

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 ;-)

Solution 2

static_cast can perform only those casts where memory layout between the classes is known at compile-time. dynamic_cast can check information at run-time, which allows to more accurately check for cast correctness, as well as read run-time information regarding the memory layout.

Virtual inheritance puts a run-time information into each object which specifies what is the memory layout between the Base and Derived. Is one right after another or is there an additional gap? Because static_cast cannot access such information, the compiler will act conservatively and just give a compiler error.


In more detail:

Consider a complex inheritance structure, where - due to multiple inheritance - there are multiple copies of Base. The most typical scenario is a diamond inheritance:

class Base {...};
class Left : public Base {...};
class Right : public Base {...};
class Bottom : public Left, public Right {...};

In this scenario Bottom consists of Left and Right, where each has its own copy of Base. The memory structure of all the above classes is known at compile time and static_cast can be used without a problem.

Let us now consider the similar structure but with virtual inheritance of Base:

class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};

Using the virtual inheritance ensures that when Bottom is created, it contains only one copy of Base that is shared between object parts Left and Right. The layout of Bottom object can be for example:

Base part
Left part
Right part
Bottom part

Now, consider that you cast Bottom to Right (that is a valid cast). You obtain a Right pointer to an object that is in two pieces: Base and Right have a memory gap in between, containing the (now-irrelevant) Left part. The information about this gap is stored at run-time in a hidden field of Right (typically referred to as vbase_offset). You can read the details for example here.

However, the gap would not exist if you would just create a standalone Right object.

So, if I give you just a pointer to Right you do not know at compile time if it is a standalone object, or a part of something bigger (e.g. Bottom). You need to check the run-time information to properly cast from Right to Base. That is why static_cast will fail and dynamic_cast will not.


Note on dynamic_cast:

While static_cast does not use run-time information about the object, dynamic_cast uses and requires it to exist! Thus, the latter cast can be used only on those classes which contain at least one virtual function (e.g. a virtual destructor)

Solution 3

Fundamentally, there's no real reason, but the intention is that static_cast be very cheap, involving at most an addition or a subtraction of a constant to the pointer. And there's no way to implement the cast you want that cheaply; basically, because the relative positions of Derived and Base within the object may change if there is additional inheritance, the conversion would require a good deal of the overhead of dynamic_cast; the members of the committee probably thought that this defeats the reasons for using static_cast instead of dynamic_cast.

Solution 4

Consider the following function foo:

#include <iostream>

struct A
{
    int Ax;
};

struct B : virtual A
{
    int Bx;
};

struct C : B, virtual A
{
    int Cx;
};


void foo( const B& b )
{
    const B* pb = &b;
    const A* pa = &b;

    std::cout << (void*)pb << ", " << (void*)pa << "\n";

    const char* ca = reinterpret_cast<const char*>(pa);
    const char* cb = reinterpret_cast<const char*>(pb);

    std::cout << "diff " << (cb-ca) << "\n";
}

int main(int argc, const char *argv[])
{
    C c;
    foo(c);

    B b;
    foo(b);
}

Although not really portable, this function shows us the "offset" of A and B. Since the compiler can be quite liberal in placing the A subobject in case of inheritance (also remember that the most derived object calls the virtual base ctor!), the actual placement depends on the "real" type of the object. But since foo only gets a ref to B, any static_cast (which works at compile time by at most applying some offset) is bound to fail.

ideone.com (http://ideone.com/2qzQu) outputs for this:

0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8

Solution 5

I suppose, this is due to classes with virtual inheritance having different memory layout. The parent has to be shared between children, therefore only one of them could be laid out continuously. That means, you are not guaranteed to be able to separate a continuous area of memory to treat it as a derived object.

Share:
20,461

Related videos on Youtube

Štěpán Němejc
Author by

Štěpán Němejc

Programmer, researcher, community organizer

Updated on July 09, 2022

Comments

  • Štěpán Němejc
    Štěpán Němejc almost 2 years

    Consider the following code:

    struct Base {};
    struct Derived : public virtual Base {};
    
    void f()
    {
        Base* b = new Derived;
        Derived* d = static_cast<Derived*>(b);
    }
    

    This is prohibited by the standard ([n3290: 5.2.9/2]) so the code does not compile, because Derived virtually inherits from Base. Removing the virtual from the inheritance makes the code valid.

    What's the technical reason for this rule to exist?

    • Lightness Races in Orbit
      Lightness Races in Orbit over 12 years
      I hope that you are content with my edit.
  • Cat Plus Plus
    Cat Plus Plus over 12 years
    This isn't really the answer, as you can downcast with static_cast in e.g. struct B {}; struct D : B {}; int main() { B* x = new D; D* y = static_cast<D*>(x); }.
  • Štěpán Němejc
    Štěpán Němejc over 12 years
    The memory layout of Derived is known at compile time, regardless of virtual inheritance being used. Also, polymorphism in general is a runtime phenomenon, yet you are still allowed to use static_cast to do things that might turn out to be undefined behavior at runtime.
  • Lightness Races in Orbit
    Lightness Races in Orbit over 12 years
    @catPlusPlus: Where's the virtual there?
  • Alok Save
    Alok Save over 12 years
    @Cat: I thin it is because the standard explicitly forbids Base class being virtual. I was just about looking for the right quote Tomalak beat me to it, Also a nitpick your example has no virtual.
  • Alok Save
    Alok Save over 12 years
    @Tomalak: Yes I see :) Actually reading the standard makes my head spin a little, I guess I can't digest it, but anyhow I try.
  • Lightness Races in Orbit
    Lightness Races in Orbit over 12 years
    @Als: I think I'm starting to become immune to it, which is dizzingly worrying.
  • Steve Jessop
    Steve Jessop over 12 years
    @eran: "The memory layout of Derived is known at compile time" - that's not sufficient, though (see my answer). Just because you've static_cast to Derived doesn't mean that the most-derived type of the object in question is Derived, and hence it might not have the layout that Derived has. That's what virtual inheritance means, that the Base sub-object isn't part of the Derived sub-object in classes derived from Derived.
  • Lightness Races in Orbit
    Lightness Races in Orbit over 12 years
    +1: Made my head hurt so I won't say it's an entirely clear answer, but appears to me to be the correct one. :)
  • Steve Jessop
    Steve Jessop over 12 years
    @Tomalak: thanks, it makes my head hurt a bit too, but if I think of a way to explain more clearly then I'll come back and edit.
  • Lightness Races in Orbit
    Lightness Races in Orbit over 12 years
    *cough* ASCII diagrams! *cough*
  • Steve Jessop
    Steve Jessop over 12 years
    @Tomalak: yeah, requires more time than I have to hand and still get to the shops during my lunch break. I'd have to actually write some classes with virtual inheritance and figure out their layouts to pin down the disparity, instead of hand-waving that part...
  • Lightness Races in Orbit
    Lightness Races in Orbit over 12 years
    Indeed. It's just about more than I'm willing to do, too, though I'd still love to see it!
  • James Kanze
    James Kanze over 12 years
    The proposed answer is incorrect. static_cast doesn't do any checking of validity; given something like static_cast<Derived*>( pBase ), if pBase doesn't actually have type Derived, the behavior is undefined. But the static_cast compiles fine.
  • James Kanze
    James Kanze over 12 years
    @Steve The only way I know of explaining the actual problem clearly is to diagram the (typical) layout in the case of Derived and Derived2. And doing such diagrams in ASCII is a pain.
  • Lightness Races in Orbit
    Lightness Races in Orbit over 12 years
    @James: No, the static_cast is ill-formed.
  • Lightness Races in Orbit
    Lightness Races in Orbit over 12 years
    Fundamentally, there's every reason.
  • David Hammen
    David Hammen over 12 years
    Not the downvoter, but I concur with Tomalak. The static in static_cast means a compile-time conversion for a non-null pointer.
  • James Kanze
    James Kanze over 12 years
    @David Hammen It depends on what you mean by "compile-time" conversion. static_cast definitely can involve some code executed at run-time, even when only pointers are involved. But as I said, the code is fairly limited: adding or subtracting a constant from the pointer, but not looking things up in the vtable. (Not sure why the down vote either, since my answer is totally correct. But votes here seem to be pretty random anyway; I don't think they mean anything.)
  • David Hammen
    David Hammen over 12 years
    @JamesKanze: I meant pretty much what you said. There will be run-time code, but it will be limited in nature: Checking for a null pointer, adding a fixed offset determined at compile time. Static cast works without RTTI. Dynamic cast in general does not. (There is some overlap; dynamic cast can be used for upcasting.)
  • James Kanze
    James Kanze over 12 years
    @David That corresponds to what I understand was the intent of the committee. Although RTTI didn't exist at the time. Roughly, for non-virtual derivation, the generated code has to check for null, then add or subtract a constant; with virtual derivation, the constant is replaced with an entry in the vtable, which requires two memory accesses to get. IMHO, they should have done it anyway; two memory accesses aren't the end of the world. But the committee felt otherwise.
  • Aaron McDaid
    Aaron McDaid almost 9 years
    Is a dynamic_cast possible in general?
  • Hemant Bhargava
    Hemant Bhargava about 7 years
    This explanation is not clear enough to understand. Can you please elaborate more on it?