A Better Visitor
VP can be improved, and there are many attempts in this direction[4], with various degrees of success. Most of them are unfortunately intrusive or not easy to generalize.
This article will introduce different methods to better deal with VP's deficiencies, most of the time relieving the u of manually creating any VP code infrastructure.
Solving D1
namespace Visitor_1 { class Hammer; class Drill; struct Visitor { virtual void visit(Hammer & h) = 0; virtual void visit(Drill & d) = 0; }; struct Visitable { virtual void accept(Visitor & v)=0; }; template <class T> struct VisitableAdapter : Visitable { VisitableAdapter(T& t) : this_(t) {} virtual void accept(Visitor & v) { v.visit(this_); } private: T& this_; }; class Hammer {}; class Drill {}; struct DoSomethingVisitor : Visitor { void visit(Hammer & h) { std::cout <<"Hammer" << std::endl; } void visit(Drill & d) { std::cout <<"Drill" << std::endl; } }; template <class V> struct accept { accept(V& v) : v_(v) {} template <class T> void operator()(T* t) { t->accept(v_); } private: V& v_; }; void doSomethingWithAllTools() { Hammer h; Drill d; VisitableAdapter<Hammer> hammer(h); VisitableAdapter<Drill> drill(d); std::vector<Visitable *> myToolBox; // filled with lots of tools myToolBox.push_back(&hammer); myToolBox.push_back(&drill); DoSomethingVisitor v; std::for_each(myToolBox.begin(), myToolBox.end(), accept<DoSomethingVisitor>(v)); } }
This example solves D1 by using external polymorphism (presented in a previous article [3]). The main idea is that unrelated classes can be forced to share an external interface. Let's see external polymorphism at work and compare this approach with the standard one.
Hammer and Drill don't implement a Tool interface anymore—they are now unconstrained, independent classes. Instead, the VisitableAdapter and the Visitable interface do all the heavy lifting in a very generic way.
Applying VP is now almost automatic:
- Unrelated classes are wrapped in VisitableAdapter.
- Visitor interface and concrete Visitors have to be written the same as in the standard case.
- Put the Visitable instances from step (a) in a container and do a traversal using any concrete Visitor from tstep (2).
Note that this method solves D1 entirely without offering a solution for D2 as VisitableAdapter has a direct dependency on Visitor.
This method works best in those cases when the ad hoc hierarchy is stable[5].
Solving D1 and D2
namespace Visitor_2 { struct NullVisitor { template <class V> void accept(V& v) {} }; template <class T, class PREVIOUS = NullVisitor> struct VisitableAdapter { VisitableAdapter(T& t, PREVIOUS* previous = 0) : this_(t), previous_(previous) {} template <class V> void accept(V& visit) { visit(this_); if (!previous_) return; previous_->accept(visit); } private: T& this_; PREVIOUS* previous_; }; struct Hammer {}; struct Drill {}; struct visit { visit() : i_(0) {} template <class T> void operator()(const T & h) const { std::cout <<"unknown tool" << std::endl; } mutable int i_; }; template <> void visit::operator()(const Hammer & h)const { i_ += 1; std::cout <<"hammer" << std::endl; } template <> void visit::operator()(const Drill & d) const { i_ += 2; std::cout <<"drill" << std::endl; } struct Saw {}; template <> void visit::operator()(const Saw & s) const { i_ += 4; std::cout <<"saw" << std::endl; } void doSomethingWithAllTools() { Hammer h; Drill d; typedef VisitableAdapter<Hammer> VAH; VAH vah(h); typedef VisitableAdapter<Drill, VAH> VAD; VAD vad(d, &vah); visit tv; vad.accept(tv); std::cout <<"state is " << tv.i_ << std::endl; tv.i_ = 0; Saw s; typedef VisitableAdapter<Saw, VAD> VAS; VAS vas(s, &vad); vas.accept(tv); std::cout <<"state is " << tv.i_ << std::endl; } }
This new approach uses entirely static polymorphism—gone is the Visitable/Visitor interface and the nasty coupling from the previous example! VisitableAdapter is responsible for two things now:
- Internal traversal (new responsibility); backward in this
- Accept visitors (in a generic manner this time)
And this is the way it works:
- Unrelated classes are wrapped and linked using VisitableAdapter (D1).
- A generic visit functor is provided (visit in our example).
- Specializations are provided for classes that have to be visited (please note that specializations are done outside the functor, meaning automatic compliance with the Open Closed Principle when extending the visitor).
- The last VisitableAdapter in the chain accepts the visitor and propagates the traversal.
When adding a new class:
- The new class is wrapped and linked:
Saw s; typedef VisitableAdapter<Saw, VAD> VAS; VAS vas(s, &vad);
- A specialization is provided[6] (D2):
template <>void visit::operator()(const Saw & s) const;
- The new VisitableAdapter accepts the visitor and propagates the traversal:
vas.accept(tv);
It's interesting to notice that this implementation is available out of the box using libraries like Boost::Fusion[4]:
namespace Visitor_3 { typedef Visitor_2::Hammer Hammer; typedef Visitor_2::Drill Drill; typedef Visitor_2::visit visit; void doSomethingWithAllTools() { Hammer h; Drill d; boost::fusion::vector<Hammer, Drill> myToolBox(h, d); boost::fusion::for_each(myToolBox, visit()); typedef Visitor_2::Saw Saw; Saw s; boost::fusion::vector<Saw> myNewToolBox(s); boost::fusion::joint_view < boost::fusion::vector<Hammer, Drill>, boost::fusion::vector<Saw> > view(myToolBox, myNewToolBox); boost::fusion::for_each(view, visit()); } }
Fusion is a library that bridges compile time with run time programming, allowing construction and traversal[7] of containers of heterogeneous types, meaning that double-dispatch constructs can be created on the spot for free:
boost::fusion::vector<Hammer, Drill> myToolBox(h, d);
creates a container keeping two elements of different types
boost::fusion::for_each(myToolBox, visit());
doing the traversal.
Adding new classes and solving D2 is actually extremely interesting. A new class will be added in a new container and an ad hoc view of both containers can be created and visited at run time:
boost::fusion::joint_view < boost::fusion::vector<Hammer, Drill>, boost::fusion::vector<Saw> > view(myToolBox, myNewToolBox); boost::fusion::for_each(view, visit());