3
votes

I copy the following text from the book More Effective C++.

Item 31: Making functions virtual with respect to more than one object.

class GameObject { ... };
class SpaceShip: public GameObject { ... };
class SpaceStation: public GameObject { ... };
class Asteroid: public GameObject { ... };

The most common approach to double-dispatching returns us to the unforgiving world of virtual function emulation via chains of if-then-elses. In this harsh world, we first discover the real type of otherObject, then we test it against all the possibilities:

void SpaceShip::collide(GameObject& otherObject)
{
  const type_info& objectType = typeid(otherObject);

  if (objectType == typeid(SpaceShip)) {
    SpaceShip& ss = static_cast<SpaceShip&>(otherObject);

    process a SpaceShip-SpaceShip collision;

  }
  else if (objectType == typeid(SpaceStation)) {
    SpaceStation& ss =
      static_cast<SpaceStation&>(otherObject);

    process a SpaceShip-SpaceStation collision;

  }
...
}

Here is the question:

Q1> Why we use static_cast here rather than obvious dynamic_cast?

Q2> Are they same in this case?

thank you

// updated //

In fact, I am more interested in question 2.

For example,

class base {};
class subclass : public base {};

base *pSubClass = new subclass;

subclass *pSubClass1 = static_cast<subClass*> (pSubClass); 

// does the static_cast do the job correctly in this case although I know we should use dynamic_cast here?

7
The real question is: why use dynamic_cast rather than static_cast in here?Lightness Races in Orbit

7 Answers

8
votes

For the record, here is the idiomatic way of doing that:

void SpaceShip::collide(GameObject& otherObject)
{
    if (SpaceShip* ss = dynamic_cast<SpaceShip*>(&otherObject)) {
        // process a SpaceShip-SpaceShip collision;
    }
    else if (SpaceStation* ss = dynamic_cast<SpaceStation*>(&otherObject)) {
        // process a SpaceShip-SpaceStation collision;
    }

    // ...
}

It's shorter, exhibits identical performance characteristics, and again, most importantly, is idiomatic C++ that won't make other programmers scratch their heads and wonder what the point is.


EDIT (in response to the OP's edit):

Yes, that is well defined behavior. Here's what the C++03 standard says, §5.2.9/8:

An rvalue of type “pointer to cv1 B”, where B is a class type, can be converted to an rvalue of type “pointer to cv2 D”, where D is a class derived from B, if a valid standard conversion from “pointer to D” to “pointer to B” exists, cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is not a virtual base class of D. The null pointer value is converted to the null pointer value of the destination type. If the rvalue of type “pointer to cv1 B” points to a B that is actually a sub-object of an object of type D, the resulting pointer points to the enclosing object of type D. Otherwise, the result of the cast is undefined.

4
votes

You've already verified the types yourself, so you don't need to use dynamic_cast. Dynamic_cast will check the types for you automatically.

1
votes

Why they chose to implement it this way, instead of the more traditional dynamic_cast I can't say, but the behavior of the two options is not necessarily the same. As written, that code only considers the actual type of the parameter, while dynamic_cast considers where the parameter falls in an inheritance tree. Consider:

struct Base { virtual ~Base() { } };
struct Intermediate : Base { };
struct Derived : Intermediate { };

int main() {
    Intermediate i;
    Base* p_i = &i;

    Derived d;
    Base* p_d = &d;

    assert(typeid(*p_i) == typeid(Intermediate)); //1
    assert(dynamic_cast<Intermediate*>(p_i)); //2

    assert(typeid(*p_d) == typeid(Intermediate)); //3
    assert(dynamic_cast<Intermediate*>(p_d)); //4
}

(1) and (2) both pass their assertions, but (3) fails while (4) succeeds. p_d points to a Derived object, so type_id yields information for a Derived object, which will not compare equal to the information for an Intermediate object. But Derived derives from Intermediate, so dynamic_cast will happily convert a pointer to Derived to a pointer to Intermediate.

To put it in terms used in the original question, if otherObject is a Frigate, which derives from SpaceShip, it will not use the "spaceship<->spaceship" collision routine. There's a good chance this is not the intended behavior; you might want Frigate to use that code, but instead you have to manually add an explicit check for that new type.

Of course, if you're only checking against types that are never inherited from, this difference goes away. Or if you just don't want polymorphic behavior (although that would make the heading somewhat misleading). In that case, this might be more performant, but that's a giant implementation detail and I certainly wouldn't put money on it in practice.


Another small, and largely inconsequential, difference occurs if the types are not polymorphic. In my above code, if you remove the virtual destructor from Base, (2) and (4) now exhibit undefined behavior. (1) and (3) remain well defined, but are now worthless; both will fail because typeid(*p_i) will yield information about Base rather than Intermediate like it used to.

0
votes

This seems like a pretty solid answer. Basically static cast is faster but doesn't do runtime type checking.

0
votes

Some compilers will generate codes that throws std::bad_cast if dynamic_cast fails. So in this case the two approaches are different. Using dynamic_cast may looks like

try {
    SpaceShip& ship = dynamic_cast<SpaceShip&>(otherObject);
    // collision logic
    return;
} catch (std::bad_cast&) {}

try {
    SpaceStation& station = dynamic_cast<SpaceStation&>(otherObject);
    // collision logic
    return;
} catch (std::bad_cast&) {}

that looks really bad.

0
votes

First, I think it's important to note that Myers is presenting this code as the first strawman solution for double dispatch before moving on to superior solutions that are not dependent on RTTI.

To answer the second question first, yes, this is equivalent to implementations using dynamic_cast.

static_cast is used here because we have already established that the object is of the targetted type, and thus don't need to pay for run-time checking again.

So why not use dynamic_cast in the first place?

My suspicion is that Myers wrote it this way because this was going to be a chain of an indefinite number of if () checks. He could have done something like @ildjarn suggests, but that would have involved declaring a new variable for every type he wanted to check it against. My suspicion is he just liked the aesthetics of what he put up better.

0
votes

Maybe I'm mistaken, but ... my understanding is that all rtti implementations involve some kind of lookup/search to find the type of an object passed to dynamic_cast or typeinfo.

Barring quantum effects, this search must take a measurable number of cycles to complete and, in the OP's code, the search result is cached, while in the dynamic_cast examples the search is repeated in each conditional.

Therefore the cached version must be faster. Keeping in mind caveats about premature optimization, I think it is also easy on the eye.

Nicht war?

PS: Tried this and it doesn't work. Hmmm. Anybody suggest why?