12

I want to implement a callback handler. Methods should be registered as easy as the following...

std::multimap<Event::Type, std::function<void()>> actions;

void EventManager::registerAction(Event::Type event, std::function<void()> action) {
    actions.insert(std::make_pair(event, action));
}

...which indeed works as intended.

But the problem with that approach is, that it is impossible to deregister a callback...

void EventManager::deregisterAction(Event::Type event, std::function<void()> action) {
    for(auto i = actions.lower_bound(event); i != actions.upper_bound(event); ++i) {
        // if action == i->second
    }
}

...because it's impossible to compare bound functions.

Lazy deregistering also won't work because the function object can't be validated.

void EventManager::handle(Event::Type event) {
    for(auto i = actions.lower_bound(event); i != actions.upper_bound(event); ++i) {
        if(i->second) // returns true even if the object doesn't exist anymore
            i->second();
    }
}

So how should I approach an implementation like this, how can the issues I encountered be avoided?

Community
  • 1
  • 1
Appleshell
  • 7,088
  • 6
  • 47
  • 96
  • 1
    Nice question. I suppose having an extra `map>` doesn't appeal to you? Or having a type `myaction` that's essentially a `pair >` ? (This is just a comment because I don't particularly like these ideas myself, but they may be a starting point...) – us2012 Sep 15 '13 at 15:40
  • A few months ago I writted a GUI events system implementation for a personal project which is like your own: https://github.com/Manu343726/cpp_lib32/blob/b5c46eb819241bcfd488f81bb19dce812b958035/code/tests/refactoring/Redesigned_event_system_test.cpp I mapped system events messages to events, and events are lists of callbacks. So its not possible (As in your case) to delete created events, but its possible to remove the handlers of the evemt. – Manu343726 Sep 15 '13 at 15:48
  • doesn't observer pattern be of any use to you? – 4pie0 Sep 15 '13 at 15:50
  • @us2012 Can you explain that further? – Appleshell Sep 15 '13 at 15:58
  • @Manu343726 That's not an option because any event can happen the whole run time. – Appleshell Sep 15 '13 at 15:59
  • There's just recently been a [*very* similar question](http://stackoverflow.com/q/18665515/420683). My comment to that question was: why not use [`boost::signals2`](http://www.boost.org/doc/libs/1_54_0/doc/html/signals2.html) or its technique of returning a handle when registering a callback? – dyp Sep 15 '13 at 16:06
  • @DyP Returning a handle is possibly the best answer I guess. In the meantime I also found a [similar question](http://stackoverflow.com/questions/15916175/stdfunction-as-callback-is-unregistration-possible?rq=1) with an answer giving the same advice. – Appleshell Sep 15 '13 at 16:15

3 Answers3

3

One rather simple (yet not completely clean) way would be to just return a handle to the callback, which under the hood is just an iterator to the element in the map. The user is then responsible for storing this handle if he wants to deregister it some day.

class CallbackHandle
{
    friend class EventManager;

public:
    CallbackHandle() = default;
    CallbackHandle(const CallbackHandle&) = delete;
    CallbackHandle& operator=(const CallbackHandle&) = delete;

    bool isValid() const { return iter_; }
    Event::Type event() const { return iter_.value()->first; }
    void invoke() const { iter_.value()->second(); }

private:
    typedef std::multimap<Event::Type, std::function<void()>>::iterator Iterator;

    CallbackHandle(Iterator iter) : iter_(iter) {}

    std::optional<Iterator> iter_;
};

CallbackHandle EventManager::registerAction(Event::Type event, 
                                            std::function<void()> action)
{
    return CallbackHandle(actions.insert(std::make_pair(event, action)));
}

void EventManager::deregisterAction(CallbackHandle handle)
{
    actions.erase(handle.iter_);
}

Instead of C++14's std::optional one could as well just use a boost::optional or a mere std::unique_ptr with nullptr as invalid value.

Due to the move-only nature of the handle type and the fact that you have to explicitly move the handle into the deregistering function, you automatically get it invalidated when deregistering and can never have a handle refering to an already deleted callback (Except for the fact of a completely destroyed EventManager object, which would need to be solved by some more intertwining of the two types).

In fact it is similar to Werner's solution, but a bit simpler. It can be the base for providing additional stuff on top of it, like higher level automatic RAII-based deregisterers and stuff, while still having access to low-level manual deregisteration when needed/wanted.

Christian Rau
  • 45,360
  • 10
  • 108
  • 185
  • Christian. The caller/client needn't know that the underlying mechanism for storage/lookup is a map. My mechanism abstracts this (via Resource, which is lightweight class simply representing a resource), as well as allowing for automatic de-registration. – Werner Erasmus Sep 16 '13 at 09:06
  • @WernerErasmus *"The caller/client needn't know that the underlying mechanism for storage/lookup is a map"* - Yeah, and he doesn't with my solution either, nothing in the interface speaks about a map (except if you're speaking about it appearing in the class definition, but hiding that is a completely different problem arsising from C++'s header model, for which methods exist, too, if need be). And as for the auto deregisteration, that can very well be built on top of a manual handle with a higher-level RAII-wrapper, but the user is still free to use manual deregistration if he wants/needs. – Christian Rau Sep 16 '13 at 09:08
  • @WernerErasmus Abstracting away implementation details from the header file is a problem completely different from merely abstracting them away from the interface (e.g. `public` methods), motivated by completely different factors. In fact his definition of `EventManager` probably has the map typedef in the class definition, too, and unless he states otherwise, using stuff like *Pimpl* for me is an optimization deferred to a later point. – Christian Rau Sep 16 '13 at 09:16
  • Christian: Apologies. I haven't read your entire post. I still think that CallbackHandle can expose less of EventManager. I might change my solution to be lighter and complete. Also, I've notice that you've removed that copy constructor and assignment operator, yet you are returning by value. Is this right? – Werner Erasmus Sep 16 '13 at 09:39
  • @WernerErasmus Yes, it employs move-semantics (returning temporaries or locals doesn't copy but move). This way the user cannot copy a handle. And because you can only deregister by *explicitly* giving away the handle (moving it into the function), you automatically get an invalid (empty) handle after deregistering and can never have a handle referring to a deregistered function (well, except if destroying the whole event manager, but that is a different problem, I guess, but one that could be tackled by intertwining the handle with the concrete manager object a bit more). – Christian Rau Sep 16 '13 at 09:47
  • @WernerErasmus In fact many handle-like things that in the past would require some kind of reference or pointer because of non-copyability can nowadays be simplified by move-semantics and move-only types, like streams, threads, or as best example `std::unique_ptr`. You cannot copy a `std::unique_ptr`, but you can perfectly return it from/pass it to functions by-value (like you do yourself, in the end). – Christian Rau Sep 16 '13 at 09:54
  • Thanks! This is somehow the solution I came up with later that day too. Also I implemented the automatic deregistration in the handles destructor like @WernerErasmus suggested. – Appleshell Sep 16 '13 at 14:26
  • Why the default ctor? – Lightness Races in Orbit Oct 18 '13 at 10:45
  • @LightnessRacesinOrbit So that you can create invalid handles, for later assignment, or as sentinel/dummy values. But Ok, one could have made it cleaner with `= default`, as an MSVC fan I'm not too used to this I have to admit. – Christian Rau Oct 18 '13 at 10:46
  • @ChristianRau: I never liked the idea of going out of ones way to allow invalid handles but, okay! – Lightness Races in Orbit Oct 18 '13 at 11:45
  • @LightnessRacesinOrbit It is true that many use cases can also be solved cleaner, like with `std::optional`, exceptions, and so on. But from a pragmatic viewpoint I just like to be able to write stuff like: `CallbackHandle handle; if(...) handle = mgr.registerAction(...);` or `return ... ? handle : CallbackHandle();` and such things. Or even just the possibility of putting handles into a standard container (yet that might also be possible without default-construction, not sure). – Christian Rau Oct 18 '13 at 11:48
  • @ChristianRau: You've convinced me (it is) – Lightness Races in Orbit Oct 18 '13 at 12:54
1

But the problem with that approach is, that it is impossible to deregister a callback...

I've had the following problem in the past. I solved it by using a list to ensure iterators weren't invalidated, and I returned an Unmapper object that was associated with the iterator in the list and unmapped when it went out of scope. Unmap would remove the iterator from the list of functions:

The unmapper had the following flavour:

//Polymorphic destruction being the only purpose....
struct Resource{ virtual ~Resource(){} };

template <class SubjectT, class ListT>
class Unmapper : public Resource
{
  public:
    Unmapper( std::shared_ptr<SubjectT> state,
      typename ListT::iterator ref ) :
      subject_( state ),
      ref_( ref )
    {
    }
    ~Unmapper()
    {
      std::shared_ptr<SubjectT> subject = subject_.lock();
      if( subject )
      {
        subject->unmap( ref_ );
      }
    }

  private:
    std::weak_ptr<SubjectT> subject_;
    typename ListT::iterator ref_;
};

And...

    typedef std::function<void()> StateNotifier;

    class StateImpl :
      public std::enable_shared_from_this<StateImpl>
      //And some others...
    {
      typedef std::list<StateNotifier> MappedCallList;

      //....
      virtual std::unique_ptr<Resource> mapToCall( const StateNotifier& callback )
      {
        MappedCallList::iterator callRef =
          mappedCalls_.insert( mappedCalls_.end(), callback );
        return std::unique_ptr<Resource>( new Unmapper<
          StateImpl,MappedCallList>( shared_from_this(), callRef ) );
      }

      //No brainer...
      void unmap( MappedCallList::iterator i ){ mappedCalls_.erase( i ); }
      //...
    };

Now the user needs to keep hold of the return value of mapToCall until he doesn't need it anymore, then RAII unmaps automagically.

One could easily modify this to use a map. The Unmapper is hidden from the client via the Resource interface, as the client only needs to "unmap" when "mapped" goes out of scope.

I've omitted irrelevant code for brevity. It can also be noted that I had many calls mapped to states, and at a higher level states lived in a map. This is irrelevant, as as with lists, maps iterators aren't invalidated, and can therefore be used during de-registration.

Werner Erasmus
  • 3,988
  • 17
  • 31
1

A simple solution is to wrap the callback object in an object that has an id member and then return the id from the registration call so that the value can be used to unregister the callback.

Another option is to use

std::map<std::pair<EventType, int>, std::function<void()>>

for the registry.

6502
  • 112,025
  • 15
  • 165
  • 265