Using callable objects to implement the callback design pattern

It is possible to implement a callback design pattern by combining function reference objects together with weak containers. This is done with a weak container of x::functionObj<signature> objects, the objects referenced by x::functionrefs and x::functionptr.

The weak container represents a collection of function objects that get invoked to report an event of some kind. Each callback gets installed as a function object, serving as its own mcguffin:

A x::weaklist typically represents a list of installed callbacks, in an unspecified order. It's also possible to use a x::weakmap or a x::weakmultimap to represent a prioritized list of callbacks, ordered by the map key.

#include <x/functionalrefptr.H>
#include <x/weaklist.H>

typedef x::functionref<void (int)> callback_t;

typedef x::functionptr<void (int)> callbackptr_t;

typedef x::weaklist<x::functionObj<void (int)>> callback_list_t;

callback_list_t callback_list=callback_list_t::create();

// ....

auto new_callback=callback_t::create([]
                                     (int callback_argument)
                                     {
                                          // Callback action
                                     });

// Install the callback:

callback_list->push_back(new_callback);

Here, the weak container represents a list of installed callbacks. new_callback is a new function reference object that's been installed in this callback container. When the last reference to the referenced function object goes out of scope and it gets destroyed, it gets automatically removed from the weak container.

Invoking all the current callbacks is accomplished simply by iterating over the weak container.

for (const auto &ptr : *callback_list)
{
    callbackptr_t p=ptr.getptr();

    if (!p.null())
    {
       p->invoke(4);
    }
}

Note

With multiple execution threads, it is possible to have a different execution thread invoke a particular callback just after the last reference to the callback object appears to go out of scope and get destroyed. Since getptr() recovers a regular, strong reference from the weak pointer the execution thread that's invoking the callbacks could end up holding the last remaining reference to the callback object, and be executing it at the same time that the last existing reference to the object goes out of scope.

It is important to understand that with multiple execution threads, it's possible for a callback to still get invoked after its object gets supposedly destroyed, for one last time.

x::invoke_callbacks() helpers

These two template functions generate the typically code for iterating over the callback container, and invoking each callback:

#include <x/invoke_callbacks.H>

x::invoke_callbacks(callback_list, 4);

x::invoke_callbacks_log_exceptions(callback_list, 0);

x::invoke_callbacks()'s first parameter is the callback list. x::invoke_callbacks() invokes each callback in the weak list, with its remaining arguments forwarded to each callback. x::invoke_callbacks() does not catch any x::exceptions thrown from an invoked callback. A thrown exception stops the iteration, and any remaining callbacks in the list from getting invoked. x::invoke_callbacks_log_exceptions() catches any x::exception that gets thrown from an invoked callback. The caught exception is logged before the next callback in the weak list gets invoked.

Callbacks that return non-void values require two extra parameters:

typedef x::weaklist<functionObj<int(int)>> callback_list_t;

callbacks_list_t callback_list=callback_list_t::create();

This is a container of callbacks that not only take an int parameter, but the callbacks also return an int parameter. x::invoke_callbacks(), and x::invoke_callbacks_log_exceptions() now take two extra lambda parameters, after the weak container passed as the first parameter (and any remaining parameters get forwarded to each callback, as usual):

int retval=x::invoke_callbacks(callbacks,
    []
    (int value)
    {
        return value < 0;
    },
    []
    {
        return 0;
    },
    "Hello world", 0);

Using x::invoke_callbacks() and invoke_callbacks_log_exceptions() with a weak list of callbacks that return a non-void value requires the following parameters:

  • The weak container of callbacks, as usual.

  • A lambda that checks the return value from each callback. If the lambda returns true, callback invocation stops. Any remaining callbacks do not get invoked, and x::invoke_callbacks() or x::invoke_callbacks_log_exceptions() returns immediately, with the last callback's return value.

  • A lambda that returns the value that x::invoke_callbacks() or x::invoke_callbacks_log_exceptions() itself returns if the weak list is empty, or if the first lambda returned false for every invoked callback's value.

  • The remaining parameters get forwarded to each invoked callback.