How Visitor Pattern Avoid Downcasting

How Visitor Pattern avoid downcasting

A bare, minimalistic example.

Before

class Base {};
class Derived1 : public Base {};
class Derived2 : public Base {};

// Some arbitrary function that handles Base.
void
Handle(Base& obj) {
if (...type is Derived1...) {
Derived1& d1 = static_cast<Derived1&>(base);
std::printf("Handling Derived1\n");
}
else if (...type is Derived2...) {
Derived2& d2 = static_cast<Derived2&>(base);
std::printf("Handling Derived2\n");
}
}

This means Base must have some type tag field, or you will be using dynamic_cast to check for each type.

After

// Class definitions
class Visitor;
class Base {
public:
// This is for dispatching on Base's concrete type.
virtual void Accept(Visitor& v) = 0;
};
class Derived1 : public Base {
public:
// Any derived class that wants to participate in double dispatch
// with visitor needs to override this function.
virtual void Accept(Visitor& v);
};
class Derived2 : public Base {
public:
virtual void Accept(Visitor& v);
};
class Visitor {
public:
// These are for dispatching on visitor's type.
virtual void Visit(Derived1& d1) = 0;
virtual void Visit(Derived2& d2) = 0;
};

// Implementation.
void
Derived1::Accept(Visitor& v) {
v.Visit(*this); // Calls Derived1 overload on visitor
}
void
Derived2::Accept(Visitor& v) {
v.Visit(*this); // Calls Derived2 overload on visitor
}

That was the framework. Now you implement actual visitor to handle the object polymorphically.

// Implementing custom visitor
class Printer : public Visitor {
virtual void Visit(Derived1& d1) { std::printf("Handling Derived1\n"); }
virtual void Visit(Derived2& d2) { std::printf("Handling Derived2\n"); }
};

// Some arbitrary function that handles Base.
void
Handle(Base& obj)
{
Printer p;
obj.Accept(p);
}
  1. Accept() is a virtual function that dispatches on the type of obj (first dispatch)
  2. It then calls appropriate overload of Visit(), because inside Accept() you already know the type of your object.
  3. Visit(), in turn, is a virtual function that dispatches on the type of visitor (second dispatch).

Because you have double dispatch (one on object, another on visitor), you don't do any casting. The downside is that any time you add a class to your hierarchy, you have to go and update your visitor class to add an appropriate function to handle the new subclass.

How to avoid downcast?

The typical visitor pattern performs no downcast, thanks to a double-dispatch strategy:

// Visitor.hpp
class EventBar;
class EventFoo;

class Visitor {
public:
virtual void handle(EventBar const&) = 0;
virtual void handle(EventFoo const&) = 0;
};

// Event.hpp
class Visitor;

class Event {
public:
virtual void accept(Visitor&) const = 0;
};

And the implementations:

// EventBar.hpp
#include <Event.hpp>

class EventBar: public Event {
public:
virtual void accept(Visitor& v);
};

// EventBar.cpp
#include <EventBar.hpp>
#include <Visitor.hpp>

void EventBar::accept(Visitor& v) {
v.handle(*this);
}

The key point here is that in v.handle(*this) the static type of *this is EventBar const&, which selects the correct virtual void handle(EventBar const&) = 0 overload in Visitor.

Should/how can I avoid downcasting in this case?

A downcast is almost always a symptom of a contradiction in the design. Your contradiction lies here:

[…] it's fine for me to use the base class as I only require basic device functionality […]

[…] I'm going to need to only work with the classes that implement manufacture specific functionality.

Apparently it's not fine for you to only know about the base class because, suddenly, it
turns out you do have to know about the more concrete type!?

By having a piece of code take a Device, you express: This piece of code works with any kind of Device. If this piece of code has to then downcast the Device it was given and check whether it is of the kind of Device it actually can deal with, then we have to ask ourselves the question: If this piece of code cannot actually work with any kind of Device, why did it accept any kind of Device as input? What happens if this code is given a Device it can't work with? A component that has to downcast in its implementation says one thing and does another… recommended reading: Liskov substitution principle.

The problem is that what kind of design would work in your particular application depends on the particular application. Without knowing more about that application, it's very hard to suggest what would be a good way to fix the design. However, here are a few thoughts:

Why store all devices in the same collection? Why not store the devices in separate collections, one for each kind? That enables you to not just display the devices to the user, but display them by category. It also means you do not throw away the information you need.

Alternatively, even if you do not know the concrete types of all objects in your data structure, the objects themselves always know what they are. The Double Dispatch pattern (basically a version of the Visitor pattern) may be of interest to you.

Finally, whenever you see an std::shared_ptr, ask yourself: Does this object really have more than one owner? Actual shared ownership scenarios should be rather rare. In your case, you seem to be storing devices in a container. Chances are that whatever contains that container is the sole owner of these devices. Thus std::unique_ptr would probably be a more appropriate choice…

How to avoid downcasting while having interface and base classes?

You can use the visitor pattern. In your example, this would be:

class DerivedA;
class DerivedB;
class Visitor
{
public:
virtual void visitA( DerivedA& a ) = 0;
virtual void visitB( DerivedB& b ) = 0;
};

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

class DerivedA : public Base
{
public:
virtual void Accept( Visitor& visitor ) { visitor.visitA( *this ); }
};

class DerivedB : public Base
{
public:
virtual void Accept( Visitor& visitor ) { visitor.visitB( *this ); }
};

Then, from AManager or BManager:

void Handle(Base* pFoo) 
{
class MyVisitor : public Visitor
{
public:
virtual void visitA( DerivedA& a )
{
// do somethiong specific to a, you have access to DerivedA
}
virtual void visitB( DerivedB& b )
{
// do somethiong specific to b, you have access to DerivedB
}
};
MyVisitor v;
pFoo->Accept( v );
}

The disadvantage of visitor pattern is that you'll have to define a new visitor class every time you'll want to do something specific.

You can also consider doing this (but I definitely recommend visitors, very helful if you add DerivedC later or want to share some specific operation through shared visitor classes).

class Base 
{
public:
virtual DerivedA* GetAsA() = 0;
virtual DerivedB* GetAsB() = 0;
};

class DerivedA : public Base
{
public:
virtual DerivedA* GetAsA() { return this; }
virtual DerivedB* GetAsB() { return NULL; }
};

class DerivedB : public Base
{
public:
virtual DerivedA* GetAsA() { return NULL; }
virtual DerivedB* GetAsB() { return this; }
};

Then, from AManager or BManager:

void Handle(Base* pFoo) 
{
if ( pFoo->GetAsA() )
{
// use GetAsA to access DerivedA object avoiding downcasting
}
if ( pFoo->GetAsB() )
{
// use GetAsB to access DerivedB object avoiding downcasting
}
}

If Downcasting Should Be Avoided, What Should I Do?

You can use the visitor pattern which frees you from casting. The idea is simple : You have an inventory of IGameObject which would have a method accept(GameObjectVisitor v) which directly calls v.visit(this). In your GameObjectVisitor, you just have to implement visit methods for each implementations : For example visit(Chargegun g), visit(Sword s), etc...

To explain it another way, it's like the principle of a boomerang : The GameObjectVisitor calls item.accept(this), and the Item implement accept(GameObjectVisitor g) with a simple g.visit(this).

By doing this, the Visitor has multiple visit methods for each implementation and can do specific stuffs without having to cast/using instanceof.



Related Topics



Leave a reply



Submit