Search code examples
c++c++11templatesvisitor-patterndouble-dispatch

Mixing double dispatch and static polymorphism


I'm sure this is a bad idea. Let's pretend I have a good reason to do it. I have a tree of nodes that successfully uses static polymorphism to pass messages. Crucially, each node cannot the types of the nodes it connects to, it just knows the types of messages it passes. To traverse the tree, I've implemented the visitor pattern using CRTP. This works for the first level of the tree.

However, when traversing to the second layer of the tree, the next node's type is erased using the below AnyNode class. I've been unable to figure out how to downcast from the erased type to the concrete type. The below example works in the tests, but I think it's also probably really dangerous and just working by the luck of where memory happens to be laid out.

It seems problematic that I have to erase the type of the visitor in AnyNode::Model<T>::acceptDispatch, which is fully known in AnyNode::Concept::accept. But I can't figure out how to downcast from the Concept to the Model in the Concept (I tried a covariant virtual cast function, but that didn't work). And I can't pass the typed Visitor to the derived Model class using a virtual method, because virtual methods can't be templated.

Is there a safe way to call node.accept and pass the visitor without having to erase the visitor's type and then static cast it back? Is there some way to downcast the Concept to a Model<T> at runtime? Is there a better way to approach this problem? Isn't there some crazy new C++11 way of solving this, possibly with SFINAE?

class AnyNode
{
    struct Concept
    {
        virtual ~Concept() = default;

        template< typename V >
        void accept( V & visitor )
        {
            acceptDispatch( &visitor );
        }

        virtual void acceptDispatch( VisitorBase * ) = 0;
    };

    template< typename T >
    struct Model : public Concept
    {
        Model( T &n ) : node( n ) {}

        void acceptDispatch( VisitorBase * v ) override
        {
            // dynamic cast doesn't work, probably for good reason
            NodeVisitor< T >* visitor = static_cast< NodeVisitor< T >* >( v );
            std::cout << "CAST" << std::endl;
            if ( visitor ) {
                std::cout << "WAHOO" << std::endl;
                node.accept( *visitor );
            }
        }

    private:
        T &node;
    };

    std::unique_ptr< Concept > mConcept;
public:

    template< typename T >
    AnyNode( T &node ) :
            mConcept( new Model< T >( node )) {}


    template< typename V >
    void accept( V & visitor )
    {
        mConcept->accept( visitor );
    }
};

EDIT here's the Visitor base classes, and an example derived visitor. The derived Visitors are implemented by client code (this is part of a library), so the base classes can't know what Visitors will be implemented. I'm afraid this distracts from the central question, but hopefully it helps explain the problem a bit. Everything in here works, except when ->accept( visitor ) is called on the AnyNode pointer in outlet_visitor::operator().

// Base class for anything that implements accept
class Visitable
{
public:
};


// Base class for anything that implements visit
class VisitorBase
{
public:
    virtual ~VisitorBase() = default;
};

// Visitor template class

template< typename... T >
class Visitor;

template< typename T >
class Visitor< T > : public VisitorBase
{
public:
    virtual void visit( T & ) = 0;
};

template< typename T, typename... Ts >
class Visitor< T, Ts... > : public Visitor< Ts... >
{
public:
    using Visitor< Ts... >::visit;

    virtual void visit( T & ) = 0;
};

template< class ... T >
class NodeVisitor : public Visitor< T... >
{
public:

};

// Implementation of Visitable for nodes

template< class V >
class VisitableNode : public Visitable
{
    template< typename T >
    struct outlet_visitor
    {
        T &visitor;
        outlet_visitor( T &v ) : visitor( v ) {}


        template< typename To >
        void operator()( Outlet< To > &outlet )
        {
            for ( auto &inlet : outlet.connections()) {
                auto n = inlet.get().node();
                if ( n != nullptr ) {
                    // this is where the AnyNode is called, and where the
                    // main problem is
                    n->accept( visitor );
                }
            }
        }
    };

public:
    VisitableNode()
    {
        auto &_this = static_cast< V & >( *this );
        _this.each_in( [&]( auto &i ) {
            // This is where the AnyNode is stored on the inlet,
            // so it can be retrieved by the `outlet_visitor`
            i.setNode( *this );
        } );
    }

    template< typename T >
    void accept( T &visitor )
    {
        auto &_this = static_cast< V & >( *this );
        std::cout << "VISITING " << _this.getLabel() << std::endl;

        visitor.visit( _this );

        // The outlets are a tuple, so we use a templated visitor which
        // each_out calls on each member of the tuple using compile-time
        // recursion.
        outlet_visitor< T > ov( visitor );
        _this.each_out( ov );
    }
};

// Example instantiation of `NodeVistor< T... >`

class V : public NodeVisitor< Int_IONode, IntString_IONode > {
public:

    void visit( Int_IONode &n ) {
        cout << "Int_IONode " << n.getLabel() << endl;
        visited.push_back( n.getLabel());
    }

    void visit( IntString_IONode &n ) {
        cout << "IntString_IONode " << n.getLabel() << endl;
        visited.push_back( n.getLabel());
    }

    std::vector< std::string > visited;
};

Solution

  • Ah, I think I see your problems now. The problem here with dynamic_cast (and static_cast as well) is that a NodeVisitor with multiple types doesn't generate all single-typed Visitor classes.

    In your provided example, class V is derrived from NodeVisitor< Int_IONode, IntString_IONode >, which will eventually generate Visitor< Int_IONode, IntString_IONode > and Visitor< IntString_IONode > classes as bases. Note that Visitor< Int_IONode > is not generated. (visit<Int_IONode> is in Visitor< Int_IONode, IntString_IONode >.) You also don't have either NodeVisitor< Int_IONode > or NodeVisitor< IntString_IONode >. Casting anything to either class will be Undefined Behavior since the class you're casting from cannot be either one of those.

    To address that you'll need to generate all the single-type Visitor classes. I think something like this might work (NOTE: not tested):

    template< typename T, typename... Ts >
    class Visitor< T, Ts... > : public Visitor< T >, public Visitor< Ts... >
    {
    public:
        using Visitor< T >::visit;
        using Visitor< Ts... >::visit;
    };
    

    This will define all of the visit methods within the single type Visitor classes.

    Next, change visitor in acceptDispatch to

    auto visitor = dynamic_cast< Visitor< T >* >( v );
    

    Since v is a VisitorBase, if everything is declared properly this should get you to the desired Visitor class and the contained visit method.