C++ type comparison: typeid vs double dispatch dynamic_cast

1.3k views Asked by At

Are there any performance or robustness reasons to prefer one over the other?

#include <iostream>
#include <typeinfo>

struct B
{
    virtual bool IsType(B const * b) const { return IsType2nd(b) && b->IsType2nd(this); }
    virtual bool IsType2nd(B const * b) const { return dynamic_cast<decltype(this)>(b) != nullptr; }
};

struct D0 : B
{
    virtual bool IsType(B const * b) const { return IsType2nd(b) && b->IsType2nd(this); }
    virtual bool IsType2nd(B const * b) const { return dynamic_cast<decltype(this)>(b) != nullptr; }
};

struct D1 : B
{
    virtual bool IsType(B const * b) const { return IsType2nd(b) && b->IsType2nd(this); }
    virtual bool IsType2nd(B const * b) const { return dynamic_cast<decltype(this)>(b) != nullptr; }
};

int main()
{
    using namespace std;
    B b, bb;
    D0 d0, dd0;
    D1 d1, dd1;

    cout << "type B  == type B  : " << (b.IsType(&bb)   ? "true " : "false") << endl;
    cout << "type B  == type D0 : " << (b.IsType(&dd0)  ? "true " : "false") << endl;
    cout << "type B  == type D1 : " << (b.IsType(&dd1)  ? "true " : "false") << endl;
    cout << "type D0 == type B  : " << (d0.IsType(&bb)  ? "true " : "false") << endl;
    cout << "type D0 == type D0 : " << (d0.IsType(&dd0) ? "true " : "false") << endl;
    cout << "type D0 == type D1 : " << (d0.IsType(&dd1) ? "true " : "false") << endl;
    cout << "type D1 == type B  : " << (d1.IsType(&bb)  ? "true " : "false") << endl;
    cout << "type D1 == type D0 : " << (d1.IsType(&dd0) ? "true " : "false") << endl;
    cout << "type D1 == type D1 : " << (d1.IsType(&dd1) ? "true " : "false") << endl;
    cout << endl;
    cout << "type B  == type B  : " << (typeid(b) == typeid(bb)   ? "true " : "false") << endl;
    cout << "type B  == type D0 : " << (typeid(b) == typeid(dd0)  ? "true " : "false") << endl;
    cout << "type B  == type D1 : " << (typeid(b) == typeid(dd1)  ? "true " : "false") << endl;
    cout << "type D0 == type B  : " << (typeid(d0) == typeid(&bb) ? "true " : "false") << endl;
    cout << "type D0 == type D0 : " << (typeid(d0) == typeid(dd0) ? "true " : "false") << endl;
    cout << "type D0 == type D1 : " << (typeid(d0) == typeid(dd1) ? "true " : "false") << endl;
    cout << "type D1 == type B  : " << (typeid(d1) == typeid(bb)  ? "true " : "false") << endl;
    cout << "type D1 == type D0 : " << (typeid(d1) == typeid(dd0) ? "true " : "false") << endl;
    cout << "type D1 == type D1 : " << (typeid(d1) == typeid(dd1) ? "true " : "false") << endl;
}

output:

type B  == type B  : true 
type B  == type D0 : false
type B  == type D1 : false
type D0 == type B  : false
type D0 == type D0 : true 
type D0 == type D1 : false
type D1 == type B  : false
type D1 == type D0 : false
type D1 == type D1 : true 

type B  == type B  : true 
type B  == type D0 : false
type B  == type D1 : false
type D0 == type B  : false
type D0 == type D0 : true 
type D0 == type D1 : false
type D1 == type B  : false
type D1 == type D0 : false
type D1 == type D1 : true 
2

There are 2 answers

0
Christophe On BEST ANSWER

From the design perspective, the double dispatch is far more flexible:

  • Currently you check for strict egality between the type with IsType2nd(b) && b->IsType2nd(this). But may be at some time you'd like to derivate further

  • But one day you may want to derivate further D1, but still want to consider it as if it where a D1 object when comparing types. This kind of special case is easily done with double dispatch.

This flexibility comes at a cost: the assembler code would use 2 indirect calls via a vtable, plus a dynamic cast.

The direct type information is not the greatest design, as Sergey pointed out: It will always be a strict type comparison, with no special case possible.

This inflexibility comes with advantage of simplicity in code generation: the code has just to get the dynamic type information at the start of the vtable (and the compiler can easily optimise this fetch away for object where the type is known at compile time.

For the sake of curiosity, here some code generated : he typeid is optimised away at compile time, whereas the double displatch still relies on indirect calls.

4
skypjack On

As stated in the comments, it follows another possible solution that neither uses typeid nor relies on dynamic_cast.

I've added a couple of additional examples to show how you can easily define a family type (as an example, here both D1 and D1Bis appear as being of the same family type, even if they are actually different types).
Not sure that's a desired feature, anyway...

Hoping it's of interest for you.

#include<iostream>

struct BB {
    virtual unsigned int GetType() = 0;

    bool IsType(BB *other) {
        return GetType() == other->GetType();
    }

protected:
    static unsigned int bbType;
};

unsigned int BB::bbType = 0;

struct B: public BB {
    unsigned int GetType() override {
        static unsigned int bType = BB::bbType++;
        return bType;
    }
};

struct D0: public B {
    unsigned int GetType() override {
        static unsigned int d0Type = BB::bbType++;
        return d0Type;
    }
};

struct D1: public B {
    unsigned int GetType() override {
        static unsigned int d1Type = BB::bbType++;
        return d1Type;
    }
};

struct D1Bis: public D1 { };

int main() {
    using namespace std;
    B b, bb;
    D0 d0, dd0;
    D1 d1, dd1;
    D1Bis d1Bis;

    cout << "type B  == type B  : " << (b.IsType(&bb)   ? "true " : "false") << endl;
    cout << "type B  == type D0 : " << (b.IsType(&dd0)  ? "true " : "false") << endl;
    cout << "type B  == type D1 : " << (b.IsType(&dd1)  ? "true " : "false") << endl;
    cout << "type B  == type D1BIS : " << (b.IsType(&d1Bis)  ? "true " : "false") << endl;
    cout << "type D0 == type B  : " << (d0.IsType(&bb)  ? "true " : "false") << endl;
    cout << "type D0 == type D0 : " << (d0.IsType(&dd0) ? "true " : "false") << endl;
    cout << "type D0 == type D1 : " << (d0.IsType(&dd1) ? "true " : "false") << endl;
    cout << "type D0 == type D1BIS : " << (d0.IsType(&d1Bis) ? "true " : "false") << endl;
    cout << "type D1 == type B  : " << (d1.IsType(&bb)  ? "true " : "false") << endl;
    cout << "type D1 == type D0 : " << (d1.IsType(&dd0) ? "true " : "false") << endl;
    cout << "type D1 == type D1 : " << (d1.IsType(&dd1) ? "true " : "false") << endl;
    cout << "type D1 == type D1Bis : " << (d1.IsType(&d1Bis) ? "true " : "false") << endl;
}