Callback Types

Now that we’ve seen how to handle callbacks with C++11, let’s take a look at various callback types and approaches.

Table of Contents:

  1. Callback with No Input Needed
  2. Callback with Variable Input
  3. Registering a Callback with a Specific Input
  4. Registering a Callback for Specific Events
  5. Putting It All Together
  6. Further Reading

Callback with No Input Needed

Many callbacks you see in the field will resemble this prototype:

void callback(uint32_t input)

However, I would say that most of the time I have ignored the input argument completely!

If you are not designing a generic API (such as an RTOS callback definition), it is better to indicate that this input is meaningless by using a void prototype.

If you are stuck with an input type, I would remind future developers that you are not using the input by making your local callback prototype name the variable unused, i.e.:

void my_callback(uint32_t unused)
{
    //do some stuff
}

Callback with Variable Input

However, there are times when your callback should receive some value that is dependent upon the object/event/function the callback is related to.

This callback requires no additional storage: just keep track of the list of std::function objects that you need to call. Simply pass the argument back at callback time.

Note

You can replace std::function with a static memory equivalent, like etl::delegate or inplace_function. You can also simply use a list of function pointers, such as if you are using C or do not need to support lambdas and bind outputs.

Registering a Callback with a Specific Input

Occasionally, you will need a callback which is registered with a specific input. Perhaps you have one handler registered to multiple callback events and need to handle each differently. Perhaps you need to know which specific message to wait for before executing specific code.

Regardless of the reason, your callback storage must be updated to handle two items:

  • std::function
  • Input value
Note

You can replace std::function with a static memory equivalent, like etl::delegate or inplace_function. You can also simply use a list of function pointers, such as if you are using C or do not need to support lambdas and bind outputs.

The storage is simple: define a container struct and use that for the internal std::vector (or equivalent) storage.

struct cb_arg_t {
    //the callback - takes a uint32_t input.
    std::function<void(uint32_t)> cb;
    //value to return with the callback.
    uint32_t arg;
};

std::vector<cb_arg_t> callbacks_;

For simplicity of the end user, you could have the registration function take two inputs:

void register_callback(const cb_t &cb, const uint32_t val)
{
    // add callback to end of callback list
    callbacks_.push_back({cb, val});
}

And then use the stored value when invoking the callbacks:

void callback() const
{
    // iterate through callback list and call each one
    for (const auto &cb : callbacks_)
    {
        cb.cb(cb.arg);
    }
}

Registering a Callback for Specific Events

The other callback type I often find myself using is registering a callback for a specific event. For example, perhaps you are working on a video pipeline, and you only need to receive notifications when the pipeline has stopped. Or perhaps you only care if the USB bus has received an error event.

Whatever the case may be, there should be a shared definition of possible events. When registering a callback, you can store the desired event. Later, our code can ensure that each callback is only called for its desired event.

enum my_events_t
{
    VIDEO_STOP = 0,
    VIDEO_START,
    EVENT_MAX
};

struct cb_event_t {
    std::function<void(uint32_t)> cb;
    my_events_t event;
};

std::vector<cb_event_t> callbacks_

Similar to the fixed input example above, simply provide two inputs in your registration function:

void register_callback(const cb_t &cb, const my_events_t event)
{
    // add callback to end of callback list
    callbacks_.push_back({cb, event});
}

However, only invoke the callback if the event type matches:

void callback(my_events_t event) const
{
    // iterate through callback list and call each one
    for (const auto &cb : callbacks_)
    {
        if(cb.event == event)
        {
            cb.cb(event); // or whatever / no value here.
        }
    }
}
Note

Many drivers do not use a single callback management point, instead allowing clients to register for each event through a different API (i.e. register_video_start_cbregister_video_stop_cb).

I do not recommend this approach for the sake of simplicity: it is easier to review and make changes in one function rather than multiple functions.

Putting It All Together

You can find examples of these callback types in callbacks.cpp, contained in the embedded-resources git repository.

Further Reading

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.