jeudi 4 février 2016

Templatized Slots and Signal Callback using C++11

I'll admit upfront that I'm wading a little out of my depth in terms of experience and understanding with this problem. I believe my questions overlap with some previous questions on the site, but given that the topic itself is quite complex and I'm seeking wisdom in regards to a specific implementation I hope you'll bear with me. Additionally, I'm using Visual Studio 2013 to compile this code.

Goal

What I'm trying to achieve:

  • A simple to use (yet for me hard to construct) generic event (slot/signal) system to use in my home rolled GUI library (consisting of a simple set of widgets).
  • Each of the widgets should be able to connect to other (non widget) systems, like the input system sending out gesture events, in their constructor.
  • The ability for each widget to register a member function to receive events. This member function is either generalized to a wide variety of event objects, or specialized to a particular type of event object.

Given my still limited understanding of these advanced programming concepts, I'm not even sure what I want is actually possible. The solution I have currently compiles, but doesn't behave as I expect. It also relies on a somewhat ugly cast to avoid object slicing. This is more or less an appropriate place to segway into the code I've so far muddled together.

Implementation

The base code structure is similar to most typical scene graphs I think. All widgets inherit from a base node object. Thus, all widgets can be added as children of other nodes. A separate class exists for the root node, which inherits from the base node object.

Templates are used to specialize callback functions that are passed from a widget to the root node. Since it is not possible to store templatized objects of differing specializations in a vector (and perhaps all stl data structures?), I use a struct derived from a non-template base class:

struct NodeEventFnBase { };

template<typename T>
struct NodeEventFn : NodeEventFnBase {

    NodeEventFn() {}; // Needed for map?

    NodeEventFn( std::function<bool( T )> && callbackFn )
        : mCallbackFn( std::move( callbackFn ) ) { }

    bool callback( T& args )
    {
        return mCallbackFn( args );
    }

    std::function<bool( T )> mCallbackFn;
};

The code below is a derived class of Node. NodeExt supports general slot/signal interaction and also temporarily stores callbacks, if the root note is not yet available (in the connectToEvent) function.

class NodeExt :
    public Node {
public:
    NodeExt( void ) {}
    virtual ~NodeExt( void ) {}

    template<typename T>
    void connectEvent( std::function<bool( T )> && callbackFn );

    //! Takes care of dangling connections to events, that have not been completed due to missing root.
    void addedToNode();

    //! adds a child to this node if it wasn't already a child of this node
    virtual void addChild( NodeExtRef node );

    void setNodeRoot( NodeExtRef nodeExt )
    {
        mNodeRoot = nodeExt;
    }

protected:

    /*
        It might seem more appropriate to use a vector to store these to-be-registered Event Listeners.
        However, using a map ensures that only a single Callback can be registered for a given type_index.
        Using a vector would require us to take of this of this matter. Memory-use/Performance difference
        is negligable.
    */
    std::map < std::type_index, std::shared_ptr<NodeEventFnBase> > mEventCallbacks;

    std::weak_ptr<class NodeExt> mNodeRoot;
};

template<typename T>
void NodeExt::connectEvent( std::function<bool( T )> && callbackFn )
{
    auto root = mNodeRoot.lock();

    if ( root )
    {
        root->connectEvent<T>( std::forward<std::function<bool( T )>>( callbackFn ) );
    }
    else {
        // Remember til later
        auto typeIndex = std::type_index( typeid( T ) );

        //callbackFn
        mEventCallbacks.emplace( std::make_pair( typeIndex, std::make_shared<NodeEventFn<T>>() ) );

    }
}

The root node code is fairly straight-forward:

class Node2DRoot :
    public ph::nodes::Node2D {
public:
    Node2DRoot( void );
    virtual ~Node2DRoot( void );

    void registerEvent( std::type_index typeIndex, std::shared_ptr<NodeEventFnBase> nodeEventFn )
    {
        if ( mEventListeners.count( typeIndex ) == 0 )
        {
            mEventListeners.emplace( std::make_pair( typeIndex, std::move( EventListeners() ) ) );
        }

        auto &listeners = mEventListeners.at( typeIndex );
        listeners.add( nodeEventFn );
    }

    //! adds a child to this node if it wasn't already a child of this node
    virtual void addChild( ph::nodes::NodeExtRef node );

    template <typename T>
    void sendEvent( T &event );

private:

    // A lovely unordered map pointing to all the nodes (and subsequent widgets) that have
    // registered to listen to various events.
    std::unordered_map<std::type_index, EventListeners> mEventListeners;
};


template <typename T>
void Node2DRoot::sendEvent( T &event ) {

    auto typeIndex = std::type_index( typeid( T ) );
    auto indexExists = mEventListeners.find( typeIndex );

    if ( indexExists != mEventListeners.end() )
    {
        // We recognize this type of event!
        indexExists->second.sendEvent<T>( event );
    }
    else {
        // Nowhere to send it to?

    }

    //return false;
}

The EventListeners class is basically a simple wrapper around a vector of EventListeners, which forwards events to any widget having previous registered for a particular type of event:

class EventListeners {
public:
    EventListeners() {};

    // Apparently, a default move constructor is not created (at least not in vs2013).
    // A move constructor is required because of the mEventListeners member variable, which
    // is a vector of unique_ptr's. A simpler work-around would be to use shared_ptr's instead,
    // but I feel this indicates that someone else might need/want to own any/some of the
    // EventListeners which is incorrect.
    EventListeners( EventListeners&& other )
    {
        mEventListeners.reserve( other.mEventListeners.size() );

        for ( auto &listener : other.mEventListeners )
        {
            mEventListeners.emplace_back( std::move( listener ) );
        }
    }

    void add( std::shared_ptr<NodeEventFnBase> );

    template <typename T>
    void sendEvent( T &event );

private:

    std::vector<std::unique_ptr<EventListener>> mEventListeners;
};

template <typename T>
void EventListeners::sendEvent( T &event )
{
    for ( auto &eventListener : mEventListeners )
    {
        // Send the event to each successive listener until a bool eats it?
        eventListener->sendEvent<T>( event );
    }
}

Finally, the EventListener ends up actually calling the callback function which is cast to the original type using the received type of Event object:

struct EventListener {
public:
    EventListener( std::shared_ptr<NodeEventFnBase> callbackFn );
    ~EventListener( void );

    template<typename T>
    void sendEvent( T &event );

    std::shared_ptr<NodeEventFnBase> mCallbackFn;
};

template<typename T>
void EventListener::sendEvent( T &event )
{
    // Shouldn't this be dynamic?
    // No virtual types?
    auto &callbackFnAccurate = std::static_pointer_cast<NodeEventFn<T>>( mCallbackFn );

    // The only way to 100% ensure a proper static_cast is to rely on an external flag and
    // check that the callbackFn struct is actually the right derived class
    callbackFnAccurate->callback( event );
}

Simple testing

To test this code I've created a simple hollow widget which implements a callback function

bool TestWidget::gestureDragBegin( GestureDragBeginEvent& event )
{
    return true;
}

which it connects to

connectEvent<GestureDragBeginEvent&>( [this]( GestureDragBeginEvent& event )
{ return gestureDragBegin( event ); } );

via a lambda function. GestureDragBeginEvent inherits from a base GestureEvent class, which inherits from an even more base Event class. To test this code out, I make the following function call:

mRootNode->sendEvent<GestureDragBeginEvent>( fakeEvent2 );

This call crashes with a bad function call exception. This happens in the callback function of the templatized NodeEventFn class, when mCallbackFn( args ) is executed. NodeEventFn::callback is called by the EventListener class.

Questions

I had hoped that the function call would proceed as expected, given that - as far as I can tell - I use the same specialization in all cases. So here are the topics I could really use some feedback on:

  • Is what I'm trying to do unwise, or perhaps even impossible? It seems to me that a specialized callback design should work (callback function argument type exactly matched received event object type), but I'm worried that perhaps the generic functionality (callback function argument type is a base class of the received event object type) might be unfeasible?
  • Vs2013 indicates in the debug info that the args parameter received in the NodeEventFn::callback function is of the base Event class. This is a bit confusing to me, as I am using GestureDragBeginEvent throughout the code that tests the implementation. It would seem that type information is being lost somewhere along the way, but where?
  • It seems potentially dangerous to have the widgets be designed to always exist in a shared_ptr wrapper, and yet use the this pointer for the callback functions. But if want to be able to create connections in a widgets constructor, I cannot rely on shared_from_this(), and instead must rely on the standard this pointer. If I properly clean up signals in a widgets destructor, I would expect this to work out ok. Am I mistaken?

Aucun commentaire:

Enregistrer un commentaire