[ Team LiB ] |
![]() ![]() |
Gotcha #45: Ambiguity Failure of dynamic_castSure, you feel guilty about it. You probably won't discuss it with your colleagues. It may even start to affect your personal relationships. But when your back is to the wall, you're dealing with a poorly designed module and impossible demands from your management, and you need to be finished yesterday, it may be time to employ a dynamic_cast. Let's say the problem has to do with the need to determine whether a particular screen object is an entry screen rather than some other type of screen. The problem is that you're in the middle of some otherwise generic code that should apply to screens in general. Your first impulse might be to augment the interface of all screen types to provide the required information: class Screen { public: //... virtual bool isEntryScreen() const { return false; } }; class EntryScreen : public Screen { public: bool isEntryScreen() const { return true; } }; // . . . Screen *getCurrent(); // . . . if( getCurrent()->isEntryScreen() ) // . . . The problem with this approach is that it legitimizes asking a prying question of a Screen object. The base Screen class is explicitly inviting maintainers to ask the personal question "Are you an EntryScreen?" With that, the floodgates are open, and future maintainers will add more prying questions (see Gotcha #98): class Screen { public: //... virtual bool isEntryScreen() const { return false; } virtual bool isPricingScreen() const { return false; } virtual bool isSwapScreen() const { return false; } // ad infinitum . . . }; Of course, the presence of such an interface pretty much guarantees it will be used: // . . . if( getCurrent()->isEntryScreen() ) // . . . else if( getCurrent()->isPricingScreen() ) // . . . else if( getCurrent()->isSwapScreen() ) // . . . It's kind of like a switch, except slower and less maintainable. A lesser evil is just to bite the bullet and perform a single dynamic_cast. The use of the cast will, one hopes, be sufficiently hidden not to inspire imitation and will be removed at some future date when the code is refactored: if( EntryScreen *es = dynamic_cast<EntryScreen *>(sp) ) { // do stuff with the entry screen... } If the cast succeeds, es will refer to an EntryScreen, which may be the actual type of the screen object or simply an EntryScreen subobject of a more specialized screen object. But what does failure mean? A dynamic_cast can produce a null result for any of four reasons. First, the cast can be incorrect. If sp doesn't refer to an EntryScreen or something derived from an EntryScreen, the cast will fail. Second, if sp is null, the result of the cast will also be null. Third, the cast will fail if we attempt to cast to or from an inaccessible base class. Finally, the cast can fail due to an ambiguity. Type conversion ambiguities are uncommon in well-designed hierarchies, but it's possible to get into trouble with hierarchies that are poorly constructed or improperly accessed. Figure 4-4 shows a simple multiple-inheritance hierarchy. We'll assume A is polymorphic (it has a virtual function) and that only public inheritance is used. In this case, a D object has two A subobjects; that is, at least one A is a nonvirtual base class: D *dp = new D; A *ap = dp; // error! ambiguous ap = dynamic_cast<A *>(dp); // error! ambiguous Figure 4-4. A multiple-inheritance hierarchy without virtual base classes. A D complete object contains two A subobjects.The initialization of ap is ambiguous, because it can refer to two reasonable A addresses. However, once we have the address of one of the two A subobjects, reference to any of the other subtypes in the hierarchy is unambiguous: B *bp = dynamic_cast<B *>(ap); // works C *cp = dynamic_cast<C *>(ap); // works No matter which A subobject ap refers to, converting it to refer to the B or C subobjects or to the D complete object is unambiguous, because the complete object contains a single instance of each of those subobjects. It's interesting to note that if both As were virtual base classes, there would be no ambiguity, since a D object would then contain a single A subobject: D *dp = new D; A *ap = dp; // OK, not ambiguous ap = dynamic_cast<A *>(dp); // OK, not ambiguous We can reintroduce ambiguity by making the hierarchy a little more complex, as in Figure 4-5. For this modified hierarchy, the earlier ambiguity is not present, because a D object still contains a single A subobject: A *ap = new D; // no ambiguity Figure 4-5. A multiple-inheritance hierarchy with virtual and nonvirtual inheritance of multiple subobjects of the same type. A D complete object contains a single A subobject but two E subobjects.However, we now have an ambiguity going the other way:
E *ep = dynamic_cast<E *>(ap); // fails!
The pointer ap could be converted to either of two E subobjects. We can circumvent this ambiguity by being more specific: E *ep = dynamic_cast<B *>(ap); // works A D contains a single B subobject, so converting an A * into a B * is unambiguous, and the subsequent conversion from B * to its public base doesn't require a cast. However, note that this solution embeds detailed knowledge of the structure of the hierarchy below classes A and E into the code. It's better to simplify the structure of the hierarchy to avoid the possibility of dynamic ambiguity. Since we're on the subject of dynamic_cast, we should point out a couple of subtleties of its semantics. First, a dynamic_cast is not necessarily dynamic, in that it may not perform a runtime check. When performing a dynamic_cast from a derived class pointer (or reference) to one of its public base classes, no runtime check is needed, because the compiler can determine statically that the cast will succeed. Of course, no cast of any kind is needed in this case, since conversion from a derived class to its public base classes is predefined. (While language rules of this type may initially seem extraneous, they often facilitate template programming, where the types to be manipulated are generally not known in advance.) It's also legal to cast a pointer or reference to a polymorphic type to void *. In this case, the result will refer to the start of the "most derived," or complete, object to which the pointer refers. Of course, we still won't know what we're pointing to, but at least we'll know where it is … ![]() |
[ Team LiB ] |
![]() ![]() |