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