Ian
Ian

Reputation: 2108

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;
};

Upvotes: 1

Views: 931

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275500

No, this isn't possible.

Suppose you have 3 modules. Module 1 is your library. Module 2 defines a node type. Module 3 defines a visitor.

They are compiled separately to binary dynamic libraries, then loaded at runtime.

If the visitor knew the node type's full type, it would be able to do arbitrary compile-time checks on the properties of the node type do change how it behaves. For example, it check at compile time if the static node_type::value encodes a proof of "P = NP" or not.

Meanwhile, nobody in the node type DLL uses node_type::value, so its very existence is optimized out (quite validly) by the compiler there.

To do what you are asking, you'd have to send not just the compiled result of node_type, but something equivalent to the entire source of node_type to the visitor DLL, and in that DLL they could recompile their visitor against this particular node_type.

If you relaxed any one of a dozen implied requirements this is doable, but you have chosen a set of incompatable asks. Quite possibly what you are asking for isn't what you actually need, you just thought to ask extremely general requests and noted that it was sufficient, then puzzled why you couldn't do it.

Upvotes: 0

1201ProgramAlarm
1201ProgramAlarm

Reputation: 32732

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.

Upvotes: 1

Related Questions