Down-casting re-visited

In my previous article I discussed the difference between up-cast and down-cast and explained why down-casting is rarely a good idea (or even necessary). That said, there are a few times when down-casting is valid and so this article shows how to do so, safely, in C++.

The first thing to ask yourself when considering a down-cast is, “do I really need to do this?”. With a few exceptions (the curious recurring template pattern being one of them) the need to down-cast is often a sign that your design is either wrong or your inheritance model needs some additional layers of abstraction. If; however, you are confident that the need to down-cast is valid you have a number of options open to you in C++, some are safer than others.

Runtime Down-casting

Static Down-cast

The most basic mechanism for down-casting is the simple static cast. Let’s look at an example:

#include <iostream>
using namespace std;

class Base
{
};

class Derived : public Base
{
    public:
        void foo() const
        {
            cout << "Hello, world" << endl;
        }
};

int main()
{
    Base && base = Derived();
    static_cast<Derived &>(base).foo();    
}

In this example, we are creating an instance of Derived and assigning it to an r-value reference of the Base type. We then use the static cast mechanism to down-cast this reference to a reference of the Derived class such that we can call the foo function. This is an inexpensive down-cast in so far as it has no runtime overhead.

The problem; however, is that Derived might not be in the same inheritance line as Base and if that happens the result of this cast is undefined. Undefined behaviour is the C++ standard’s way of saying your code is defective! Unfortunately, undefined behaviour doesn’t mean the application will crash. In fact, you may see no initial ill effects at all but that doesn’t mean all is right and, eventually, the result of an incorrect cast will lead to tears.

Dynamic down-cast

Fortunately, C++ provides a mechanism for making this cast safe. This mechanism is the dynamic cast. One thing to note is that dynamic cast only works if the inheritance model is polymorphic. This means that at least one of the functions in the inheritance tree has to be virtual otherwise the compiler will spit out a compile time error.

Let’s look at a modified version of the previous code that makes use of dynamic cast:

#include <iostream>
#include <typeinfo>
using namespace std;

class Base
{
    public:
        virtual ~Base() {};
};

class Derived : public Base
{
    public:
        void foo() const
        {
            cout << "Hello, world" << endl;
        }
};

int main()
{
    Base * base = new Derived();
    if(Derived * derived = dynamic_cast<Derived *>(base))
    {
        derived->foo();
    }
    else
    {
        cerr << "cast failed" << endl;
    }
    delete base;
}

How does this differ from the previous example? There are two changes; the base class now implements a virtual destructor to make the inheritance model polymorphic and the cast is now performed using the dynamic cast. You’ll note that the cast is now wrapped by a try/catch block.

The way dynamic cast works is to use RTTI (Run Time Type Information) to determine if the cast is safe. If it is the cast is performed. If it isn’t then a “bad_cast” exception is thrown. Of course, your code still has to deal with the fact the cast has failed but at least we’re not into the weeds of undefined behaviour and, in the worse case, your program can fail gracefully.

It should be noted that the exception is only thrown when attempting to incorrectly cast a reference type. In the case of a pointer type the result of the cast will be a nullptr rather than the throwing of an exception. The following is an example using pointers rather than references:

#include <iostream>
#include <typeinfo>
using namespace std;

class Base
{
    public:
        virtual ~Base() {};
};

class Derived : public Base
{
    public:
        void foo() const
        {
            cout << "Hello, world" << endl;
        }
};

int main()
{
    Base * base = new Derived();
    if(Derived * derived = dynamic_cast<Derived *>(base))
    {
        derived->foo();
    }
    else
    {
        cerr << "cast failed" << endl;
    }
    delete base;
}

It should be pretty obvious what’s happening here. If the cast works the foo function is called, else an error message is sent to stderr. Of course, this is just an example and so your code would need to take the necessary action in the else clause to gracefully handle the situation of the cast failing.

Notice something interesting about this code? Notice how we are able to define a new variable as part of the test expression in the ‘if’ statement and then use that instance in the code-block (in this case to call foo)? This syntax was specially added to the C++ standard to allow exactly this idiom of casting and allowing some specific action if the cast works whilst restricting the scope of the temporary pointer to the place where it’s being used.

So, we now have a safe mechanism for implementing down-casting… that’s it, right? Well, actually, no it’s not. The problem here is that the invalid down-cast is detected at runtime, which means your code must be written to deal with the failure and by then it’s too late (normally) to do anything sensible other than fail gracefully. The dynamic cast is also hugely costly in terms of runtime performance. You do not want to do this in code that is called frequently!

Can we do better? You bet we can!

Compile-time Down-casting

What we really want is a type safe way to perform this down-cast such that if it’s going to fail we’ll get a compile time error. In this way, we know that if our code compiles the cast must be valid and it will never fail at runtime.

But, wait… dynamic casting is a runtime thing isn’t it? Static casting is compile time but that won’t trap errors at all and will just result in a badly behaved app if the cast is not valid. That’s it then isn’t it? All our options are used up? Actually, no – we have one more trick up our sleeve; down-casting using the Visitor Pattern.

Visitor Pattern Down-casting

The Visitor Pattern was originally conceived as a way of divorcing an algorithm from the object to which that algorithm is being applied. Normally, when you implement an object you implement methods for that object that manipulate it. If you need to change the algorithms that perform this manipulation you have to change the object.

The visitor pattern keeps the two things separate such that you can modify the algorithms without needing to modify the objects that they are applied to. This means you can add new algorithms to be applied to an object without needing to make any changes to the object.

The Visitor Pattern works using “Double Dispatch“. Ordinarily, when you call a virtual function on an object the function called depends on the dynamic type of the object to which the function belongs. No account is taken of the dynamic type(s) being passed into the function. This is called “Single Dispatch“.

In case it isn’t clear, the static type is the concrete type of the object and the dynamic type is the type it actually references. In the example code show so far, base has the static type of Base and the dynamic type of Derived since that is the type that it actually references.

In the Visitor Pattern both the dynamic type of the object to which the function belongs and the dynamic type of the object(s) being passed to the function determine which function override gets called. Some programming languages implement this natively; C++ does not. The Visitor Pattern allows us to emulate this by making use of the C++ type system.

The following is the same example modified to use the Visitor Pattern:

#include <iostream>
#include <typeinfo>
using namespace std;

class Derived;

class Visitor
{
    public:
        void visit(Derived & derived);
};

class Base
{
    public:
        virtual ~Base() {};
        virtual void accept(Visitor && visitor) = 0;
};

class Derived : public Base
{
    public:
        void accept(Visitor && visitor)
        {
            visitor.visit(*this);
        }

        void foo() const
        {
            cout << "Hello, world" << endl;
        }
};

void Visitor::visit(Derived & derived)
{
    derived.foo();
}

int main()
{
    Base && base = Derived();
    base.accept(Visitor());
}

Ok, I’ll be the first to admit that the visitor pattern isn’t pretty when compared to the elegance of a dynamic cast. It is; however, a significant improvement in terms of type safety. You see, as long as this compiles we know that it will work just fine at runtime. But, how does it work?

As you can see Base declares a pure virtual function called accept, which is then implemented in the derived class. The purpose of this function is to “accept” the visitor object. The visitor is passed into the accept function, which will be called in the context of the dynamic type. In this case, it’ll be called in the context of Derived.

Once in this context the “visit” method on the visitor object is called and into that we pass a reference (or pointer) to the dynamic type of the object accepting the visitor. This means that the visitor is passed the correct context of the dynamic type, so that it can now treat it as a static type. In other words, although we started out with a reference to Base the visitor ends up with a reference to Derived and, as such, can safely deal with it in that context. The down-cast has been completed.

But wait… what if we were to invoke this Visitor-cast on an invalid type? Well, it should be pretty obvious! There is no method on the visitor for accepting any type other than Derived and so we’d just get a compile time error. In other words, if the cast is invalid the code won’t build.

To add support for different types that can be safely down-cast from Base you just need to add additional overloaded visit functions to the visitor. In each case the visit function will be called in the correct context of the dynamic type and, as such, you can perform whatever action is valid for that type.

Let’s look as an example for multiple derived types:

#include <iostream>
#include <typeinfo>
using namespace std;

class Derived1;
class Derived2;

class Visitor
{
    public:
        void visit(Derived1 & derived);
        void visit(Derived2 & derived);
};

class Base
{
    public:
        virtual ~Base() {};
        virtual void accept(Visitor && visitor) = 0;
};

class Derived1 : public Base
{
    public:
        void accept(Visitor && visitor)
        {
            visitor.visit(*this);
        }

        void foo() const
        {
            cout << "Hello, Derived1::foo" << endl;
        }
};

class Derived2 : public Base
{
    public:
        void accept(Visitor && visitor)
        {
            visitor.visit(*this);
        }

        void bar() const
        {
            cout << "Hello, Derived2::bar" << endl;
        }
};

void Visitor::visit(Derived1 & derived)
{
    derived.foo();
}

void Visitor::visit(Derived2 & derived)
{
    derived.bar();
}

int main()
{
    Base && base1 = Derived1();
    base1.accept(Visitor());

    Base && base2 = Derived2();
    base2.accept(Visitor());
}

Here we have two different concrete types that both derive from Base. If Base references Derived1 we want to call its member function foo(). If Base references Derived2 we want to call its member function bar(). Since these two functions are not common to Base we need to perform a down-cast to the appropriate type. The visitor take care of that for us.

Of course, the Visitor Pattern isn’t perfect. It necessitates that your objects be modified to support it and the resultant code can be a little convoluted. That all said the times you need to down-cast should be few and far between and in this authors humble opinion the ability to ensure your down-casts are safe at compile time far out weights the cons.

One thought on “Down-casting re-visited

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.