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);
}
Accept()
is a virtual function that dispatches on the type ofobj
(first dispatch)- It then calls appropriate overload of
Visit()
, because insideAccept()
you already know the type of your object. 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
In C/C++, Are Volatile Variables Guaranteed to Have Eventually Consistent Semantics Betwen Threads
What Is the Fastest Way to Compute Large Power of 2 Modulo a Number
Is Returning Local Static Object Thread Safe for Pre-C++11 Compilers
Could We Use Extern "C" in C File Without #Ifdef _Cplusplus
Are There in X86 Any Instructions to Accelerate Sha (Sha1/2/256/512) Encoding
C++ Regular Expressions with Boost Regex
Undefined Reference to Symbol '_Zn5Boost6System15System_Categoryev' Error
What's the Real Use of Using N[C-'0']
Math Interface VS Cmath in C++
G++ Can't Find Boost Libraries. I Say They'Re in Plain Sight
Two Library of Different Versions in an Application
Copy Constructor of Derived Qt Class
Linux Ipc - Multiple Writers, Single Reader
Some Issues About Cygwin[Linux in Windows] (Socket,Thread,Other Programming and Shell Issues)
Gcc Specs File: How to Get The Installation Path
In C/C++, Are Volatile Variables Guaranteed to Have Eventually Consistent Semantics Betwen Threads