From boris at kolpackov.net Thu Dec 18 12:10:01 2003
From: boris at kolpackov.net (Boris Kolpackov)
Date: Thu Dec 18 12:07:45 2003
Subject: dynamic_cast
Message-ID: <20031218181001.GA6686@kolpackov.net>
Good day,
In my recent project (compiler) I got exposed to some obscure
features of C++ dynamic_cast operator. After encountering a few
surprises I decided to test dynamic_cast in static (pun intended)
and here are some of my observations.
First of all, dynamic_cast has four distinguishable outcomes:
* run-time failure
Results in std::bad_cast exception in case of a reference and
0 in case of a pointer.
* run-time conversion
Results in an accordingly adjusted at run-time reference or
pointer.
* compile-time failure
Diagnosed by a compiler as an error.
* compile-time conversion
Results in an accordingly adjusted at compile-time reference or
pointer. Note that in this case no run-time checking is performed.
The latter two outcomes may be a surprise to you but here is the
case (and the only case, to my knowledge) in which everything is
done at compile-time:
struct A
{
virtual ~A () {}
};
struct B1 : A
{
};
struct B2 : A
{
};
struct C : B1, B2
{
};
void f ()
{
C c;
dynamic_cast (c); // compile-time error: ambiguous
B1& b1 = c;
dynamic_cast (b1); // compile-time conversion
};
This case is described in clause 5.2.7 paragraph 5 of The C++ Standard
and boils down to conversion to accessible unambiguous base. Note also
that, even though C doesn't have unambiguous base of type A, the
conversion still succeeds.
Another interesting feature is dynamic_cast to void* (described in
clause 5.2.7 paragraph 7). One might expect that it would be
equivalent to implicit conversion to void*. The following example
examines the difference:
struct A
{
virtual ~A () {}
long a;
};
struct B
{
virtual ~B () {}
long b;
};
struct C : A, B
{
};
int main ()
{
C c;
C* cp = &c;
B* bp = cp;
void* sv = bp;
void* dv = dynamic_cast (bp);
cerr << "cp = " << cp << endl;
cerr << "bp = " << bp << endl;
cerr << "sv = " << sv << endl;
cerr << "dv = " << dv << endl;
};
On my box the example prints
cp = 0xbffffa50
bp = 0xbffffa58
sv = 0xbffffa58
dv = 0xbffffa50
which shows that dynamic_cast returns pointer to the most
derived object pointed to by the argument. Note that the argument
should be a pointer/reference to a polymorphic type.
Protection violation and ambiguity are the common causes for
conversion failure. As surprising it may sound, in dynamic_cast
they lead to run-time failures. Consider the following example
(covers protection violation - ambiguity is analogous):
struct A
{
virtual ~A () {}
};
struct B
{
virtual ~B () {}
};
struct C : A, protected B
{
};
void g ()
{
C c;
A& a = c;
B& b = (B&)(c); // subvert protection
dynamic_cast (a); // run-time failure
dynamic_cast (b); // run-time failure
}
struct D : A, B
{
};
struct E : protected D
{
void f ()
{
A& a = *this;
dynamic_cast (a);
}
};
void h ()
{
E e;
A& a = (A&)(e); // subvert protection
dynamic_cast (a); // run-time failure
e.f (); // run-time failure even though executed
// in member function
dynamic_cast (a); // ok
};
The difference between the last two conversions is somewhat subtle
and specified in clause 5.2.7 paragraph 5. To better understand all
these cases you can view inheritance as a graph and dynamic_cast as
a traverser that can navigate through edges in any direction but only
if they are public and unambiguous. Plus one small condition: in order
for dynamic_cast to jump to a sibling (from A to B in D-inheritance) it
has to be able to traverse to the most derived object (E in our case).
You may also be surprised that certain 'obvious' cases are not handled
at compile-time. Consider for instance this example:
struct A
{
virtual ~A ()
{
}
};
struct B : protected virtual A
{
};
void k ()
{
C c;
A& a = (A&)(c);
dynamic_cast (a); // run-time failure
}
Since B has only protected base of type A then dynamic_cast (A&)
should always fail. Apparently this is not the case:
struct D : virtual A
{
};
struct E : D, B
{
};
void l ()
{
E e;
D& d = e;
A& a = d;
dynamic_cast (a); // ok
}
In this example compiler cannot take A->B path. Instead it takes
longer but legal path A->D->E->B.
This also shows that protection information is preserved in translated
programs and not discarded after static analysis.
Another relevant to dynamic_cast C++ feature is virtual base class. As
you may know, we cannot use static_cast to 'up-cast' from virtual base
to derived and dynamic_cast is the only option:
struct A
{
virtual ~A () {}
};
struct B : virtual A
{
};
void m ()
{
B b;
A& a = b;
static_cast (a); // compile-time failure
dynamic_cast (a); // ok
}
To understand why we can't use static_cast with virtual bases consider
this example:
struct C : virtual A
{
};
struct D : C, B
{
};
Here either C or B (or both) will have to 'give up' their instance of
A in order to share the common copy. As a result compiler has no way
to decide at compile-time whether a pointer to A is B's own copy of A
(as in the former case) or it is somebody else's copy of A that B is
sharing (as may happen in the latter case).
And finally, to show how crafty dynamic_cast can be, one practical
example:
struct A
{
virtual ~A () {}
};
struct B : A
{
};
namespace N
{
struct B
{
virtual ~B () {}
};
void f (A& n)
{
dynamic_cast (n);
}
}
Do you see the problem? Do you think compiler will warn you? Of
course, you may say, A and N::B are two unrelated types thus compiler
will never be able to convert A& to N::B&! Well, this is not quite
correct. The following definition would establish the relationship
between A and N::B which is just good enough for dynamic_cast:
struct C : A, N::B
{
};
And since compiler cannot foresee that there is no such relationship
it has no other choice than to perform run-time check.
hth,
-boris
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 652 bytes
Desc: Digital signature
Url : http://www.kolpackov.net/pipermail/notes/attachments/20031218/9d558524/attachment.bin