Skip to main content
GameDev.net gamedev.net
🔒 Locked

CRTP and dynamic dispatch

Started by ApochPiQ Nov 20, 2009 at 9:22 AM 3 replies 5k views
Original Post
ApochPiQ
ApochPiQ
I'm working on a set of C++ classes that represent nodes in a tree. I want to be able to traverse that tree and apply logic to each node, basically as a Visitor pattern:
template <typename T>
void Node::Visit(T& visitor)
{
   visitor.DoStuff();
}
However, I also want to parameterize the actual visitor call, so that it can do different things based on the specific Node subclass that it's been issued:
class Visitor
{
public:
   template <typename T>
   void DoStuffWithNode(T& node)
   {
      T.DoSomething();
   }
};
In order to pull this off, I've introduced CRTP to allow the node classes to pass their own types into the visitor:
class Traverser
{
public:
   template <typename T>
   void DoStuff(T& node)
   {
      node.Stuff();
   }
};

template <typename SelfT>
class SelfAware
{
protected:
   SelfT& GetSelf() { return *static_cast<SelfT*>(this); }
};

class SomeNode : public SelfAware<SomeNode>
{
   template <typename T>
   void Traverse(T& traverser)
   {
      traverser.DoStuff(GetSelf());
   }
};
Now it gets messy. I need to be able to store base Node pointers, which means I lose the type information about the derived node subclasses. The vast majority of things that the Node does happen through virtual functions. What I've run into is that it's nasty to try and mix CRTP and base class pointers. The best solution I've come up with is to have a base TraversableBase class which has a pure virtual function for the "Do Stuff" side of things. I then have a template Traversable<> which inherits from TraversableBase and provides the CRTP parameter. The final result looks thusly:
class ValidationTraverser
{
public:
	template <class OperationClass>
	void TraverseNode(const OperationClass& op)
	{
		TaskSafetyCheck(op, *this);
	}

	// ... etc.
};

class TraversableBase
{
public:
	virtual ~TraversableBase()
	{ }

	virtual void Traverse(Validator::ValidationTraverser& traverser) = 0;
};

template <class SelfType>
class Traversable : public TraversableBase
{
public:
	virtual void Traverse(Validator::ValidationTraverser& traverser)
	{
		traverser.TraverseNode(*static_cast<SelfType*>(this));
	}
};
This works great and accomplishes precisely what I want it to. There is, however, one serious drawback: the Node classes are now dependent on the actual "traverser" class. In this case, you can see the ValidationTraverser class showing up all over the place. I want to add several other types of traverser classes over time, which means I'd have to add overloads for all of those traverser types in the TraversableBase/Traversable interfaces. Whew! So... all that to say, is there a better way to accomplish this? Or is it just yet another corner case where C++ kind of sucks?
_moagstar_
_moagstar_
That is most certainly a tough one. I guess you are trying to mix static and dynamic polymorphism, and something has to give somewhere.

I'm not sure how restrictive this would be, but could you make the Visitors aware of the various different node types? If I've understood correctly why you want to preserve the type information (and if providing your visitors with hard-coded information about the various node types isn't a problem) something like this might work :

// #includes removed for brevitystruct NodeBase{    template <typename T>    void visit(T& visitor)    {         visitor(*this);     }    virtual std::string name() = 0;};struct Node1 : public NodeBase{    virtual std::string name()      {         return "Node1\n";     }};struct Node2 : public NodeBase{    virtual std::string name()      {         return "Node2\n";     }};struct ExceptionalNode : public NodeBase{    virtual std::string name()      {         return "ExceptionalNode\n";     }};template <typename Nodes>struct Visitor{    template <typename Base>    struct Caster    {        Caster(Base& b, Visitor<Nodes>& v) : b(b), v(v) {}        Base& b;        Visitor<Nodes>& v;        template <typename T>        void operator()(const T&) const        {            // or some other way of comparing polymorphic types            // if type_info comparisons are too expensive            if (typeid(b) == typeid(T))            {                v.go(static_cast<T&>(b));            }        }    };    template <typename T>    void operator()(T& node)    {        Caster<T> caster(node, *this);        boost::mpl::for_each<Nodes>(caster);    }    template <typename T>    void go(T& node)    {        std::cerr << node.name();    }    void go(ExceptionalNode& node)    {        // these nodes do something different, therefore        // we need to keep the type information        std::cerr << "Exceptional Node does something different\n";    }};int main(int argc, char* argv[]){    boost::scoped_ptr<NodeBase> node1(new Node1);    boost::scoped_ptr<NodeBase> node2(new Node2);    boost::scoped_ptr<NodeBase> node3(new ExceptionalNode);    Visitor< boost::mpl::vector<Node1, Node2, ExceptionalNode> > visitor;    node1->visit(visitor);    node2->visit(visitor);    node3->visit(visitor);}


The node types are an mpl vector so that it's not too restrictive.
ApochPiQ
ApochPiQ
Well, one of the big motivating factors for trying this hideous hack in the first place is that there are quite a few different node classes. The benefit of templating TraverseNode in the final example is that we can automatically generate code as needed for each node class.
Telastyn
Telastyn
Two quick questions...

What's the reason for the visitor call to be parametrized by type? Is there any way that it could be parametrized by data (eliminating/easing the second dispatch?)

Is the traverser really going to vary that much? Would it be simpler to embed that per node logic as part of the nodes themselves?
ApochPiQ
ApochPiQ
The visitor call is parameterized so that different node types can respond differently to the visitation/traversal call. For instance:

template <class OperationClass>void TraverseNode(const OperationClass& op){	TaskSafetyCheck(op, *this);}


In this case, TaskSafetyCheck is specialized for a few classes that need extra handling, such as emitting detailed error reports should things go south, or passing on the traversal call to specially nested node objects. There's a fair amount of this going on so it works better than manually creating the function overloads for various node types.


As for the traverser implementations - originally I had the logic implemented in simple virtual functions in each Node subclass. However, this introduces two serious drawbacks. First, it means that every node class has to implement a virtual function for each traverser class (at least in my situation); I'm hoping to decouple the concept of traversal from the Node implementations so that traversers can do anything they want without the node itself needing to understand what's going on.

The second issue I had with the simple virtual function approach is that it scatters implementation detail of the traversal process all over the codebase. Some of the traverser-applied logic is quite involved, and I really would like to see it centralized rather than implemented across dozens of different node types.

For example, I have a traversal process which serializes the node tree to disk. Currently this is done by invoking a virtual serializer function on every single node; this means that all nodes have to understand how to serialize themselves, which further means that any code that uses the node system now has an implicit dependency on the serialization code. With this new approach, I can collect all serialization-related logic into a single location. If I get things working as I'd really like to, there would no longer be any implicit dependency between nodes and serialization logic.

It may sound a bit backwards (after all, standard OOP philosophy is that the object should know how to frobnicate itself in all cases) but for the sake of organization I think it works well.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.