Search code examples
c++c++11callbacksynchronizationraii

Using RAII for callback registration in c++


I'm using some API to get a notification. Something like:

NOTIF_HANDLE register_for_notif(CALLBACK func, void* context_for_callback);
void unregister_for_notif(NOTIF_HANDLE notif_to_delete);

I want to wrap it in some decent RAII class that will set an event upon receiving the notification. My problem is how to synchronize it. I wrote something like this:

class NotifClass
{
public:
    NotifClass(std::shared_ptr<MyEvent> event):
        _event(event),
        _notif_handle(register_for_notif(my_notif_callback, (void*)this))
        // initialize some other stuff
    {
        // Initialize some more stuff
    }

    ~NotifClass()
    {
        unregister_for_notif(_notif_handle);
    }

    void my_notif_callback(void* context)
    {
        ((NotifClass*)context)->_event->set_event();
    }

private:
    std::shared_ptr<MyEvent> _event;
    NOTIF_HANDLE _notif_handle;
};

But I'm worried about the callback being called during construction\destruction (Maybe in this specific example, shared_ptr will be fine with it, but maybe with other constructed classes it will not be the same).

I will say again - I don't want a very specific solution for this very specific class, but a more general solution for RAII when passing a callback.


Solution

  • Your concerns about synchronisation are a little misplaced.

    To summarise your problem, you have some library with which you can register a callback function and (via the void* pointer, or similar) some resources upon which the function acts via a register() function. This same library also provides an unregister() function.

    Within your code you neither can, nor should attempt to protect against the possibility that the library can call your callback function after, or while it is being unregistered via the unregister() function: it is the library's responsibility to ensure that the callback cannot be triggered while it is being or after it has been unregistered. The library should worry about synchonisation, mutexes and the rest of that gubbins, not you.

    The two responsibilities of your code are to:

    • ensure you construct the resources upon which the callback acts before registering it, and
    • ensure that you unregister the callback before you destroy the resources upon which the callback acts.

    This inverse order of construction vs destruction is exactly what C++ does with its member variables, and why compilers warn you when you initialise them in the 'wrong' order.

    In terms of your example, you need to ensure that 1) register_for_notif() is called after the shared pointer is initialised and 2) unregister_for_notif() is called before the std::shared_ptr (or whatever) is destroyed.

    The key to the latter is understanding the order of destruction in a destructor. For a recap, checkout the "Destruction sequence" section of the following cppreference.com page.

    • First, the body of the destructor is executed;
    • then the compiler calls the destructors for all non-static non-variant members of the class, in reverse order of declaration.

    Your example code is, therefore "safe" (or as safe as it can be), because unregister_for_notif() is called in the destructor body, prior to the destruction of the member variable std::shared_ptr<MyEvent> _event.

    An alternative (and in some sense more clearly RAII adherent) way to do this would be to separate the notification handle from the resources upon which the callback function operates by splitting it into its own class. E.g. something like:

    class NotifHandle {
     public:
       NotifHandle(void (*callback_fn)(void *), void * context)
           : _handle(register_for_notif(callback_fn, context)) {}
    
       ~NotifHandle() { unregister_for_notif(_handle); }
    
     private:
       NOTIF_HANDLE _handle;
    };
    
    class NotifClass {
     public:
       NotifClass(std::shared_ptr<MyEvent> event)
           : _event(event),
             _handle(my_notif_callback, (void*)this) {}
    
       ~NotifClass() {}
    
       static void my_notif_callback(void* context) {
         ((NotifClass*)context)->_event->set_event();
       }
    
    private:
        std::shared_ptr<MyEvent> _event;
        NotifHandle _handle;
    };
    

    The important thing is the member variable declaration order: NotifHandle _handle is declared after the resource std::shared_ptr<MyEvent> _event, so the notification is guaranteed to be unregistered before the resource is destroyed.