Callbacks abound in embedded system design. You can find all sorts of use cases:
- Registering for a notification when a specific event happens (e.g. starting to record video)
- Registering for a notification when an asynchronous event has completed (e.g. asynchronous USB transfer complete callback, DMA transfer complete callback)
- Callbacks as handlers for interrupt, timer, and other OS services
Many of the C callback implementations that I have encountered throughout my career are very simplistic: they only allow for one pointer to be registered! For example:
typedef void (*func_ptr_t)(uint32_t);
static func_ptr_t cb_ptr_ = NULL
int register_cb(func_ptr_t my_func)
{
int r = -1;
if (cb_ptr_ == NULL)
{
cb_ptr = my_func;
r = 0;
}
//else - already in use! Fail!
return r;
}
If we are limited to C, the easiest way to expand upon this basic callback registration implementation is to use a linked list library and maintain a list of callbacks. This approach requires the usual linked list overhead: translating back and forth between the list pointer and the container struct.
Let’s see how C++ can help us fix this with very little typing.
Table of Contents:
- Storing Callbacks
- Storing Function Pointers
- Examples of Registering Functions with C++11
- Supporting Unregister Functionality
- Putting It All Together
- Further Reading
Storing Callbacks
What’s the simplest way to maintain a list of callbacks? Manage it with std::vector!
With std::vector, you can simply use one function to push the callback onto the list:
std::vector<cb_t> callbacks_;
// Register a callback.
void register_callback(const cb_t &cb)
{
// add callback to end of callback list
callbacks_.push_back(cb);
}
If dynamic memory allocation isn’t available for your system, take a look at the Embedded Template Library (ETL). The ETL provides a vector type which uses static memory allocation.
Storing Function Pointers
C++11 provides a std::function type. This type can be considered a safer version of a function pointer, and can reference any type of Callable target.
Instances of std::function can store, copy, and invoke any Callable target – functions, lambda expressions, bind expressions, or other function objects, as well as pointers to member functions and pointers to data members.
Like many of the C++11 features that I enjoy, std::function provides much better type safety than a simple raw function pointer. You can’t call the function with the wrong type/number of arguments!
In all of the examples provided here, I use the following typedef for cb_t:
typedef std::function<void(uint32_t)> cb_t;
This means that cb_t will accept all function signatures that take a uint32_t as input and do not return a value.
Storing Function Pointers Without Dynamic Memory
One downside to using std::function is that functional objects after a certain size will cause a dynamic memory allocation to occur.
If you cannot use dynamic memory allocation schemes on your system, there are static memory alternatives available:
- SG14’s
inplace_function.h, which I have used on numerous Embedded Artistry projects - ETL’s
delegatetype
Both types will allow you to store any type of Callable object without dynamic memory allocation.
Examples of Registering Functions with C++11
As the C++ spec detailed above, the beauty of using std::function is that you can handle any type of function object you can think of.
Static Functions
Since the instance pointer is not required, registering a static class method is about as simple as can be. Consider this simple class:
class Client {
public:
static void func(uint32_t v) const
{
printf("static instance member callback: %u\n", v);
}
};
Registering the callback simply requires referencing the function pointer of the static member function:
register_callback(&Client::func);
Lambda Functions
Lambda functions are also called “anonymous functions”. They can be defined at any point and are not tied to a function name. It is often useful to create small lambda functions for simple purposes, such as running code on a dispatch queue or registering a callback.
Thanks to std::function (or a static memory equivalent), we are able to use lambda functions as a callback:
// register a lambda function as a callback
register_callback(
[](uint32_t v) {
printf("lambda callback: %u\n", v);
});
Instance Member Functions
std::bind is another C++11 addition. std::bind allows you to create a std::function object that acts as a wrapper for the target function (or Callableobject). std::bind also allows you to keep specific arguments at fixed values while leaving other arguments variable.
std::bind also allows you to create a function object using both the instance and method pointers! This grants you the ability to specify a specific object’s member function as the callback function.
Consider this simple class below:
class Client {
public:
void func(uint32_t v) const
{
printf("instance member callback: %u\n", v);
}
};
If we wanted to register a callback to a specific client object’s func, we should use the following:
Client c;
register_callback(std::bind(&Client::func, &c, std::placeholders::_1));
We are binding Client::func with the instance pointer (&c). The std::placeholders::_1 argument notes that we intend for the uint32_t input to remain a variable value.
C Functions
What if you wanted to interface some C functions with your C++ callback?
Simply denote the function as extern "C":
extern"C" void c_client_callback(uint32_t v);
And register the callback normally:
register_callback(&c_client_callback);
If you need to register the callback code from a C directly, you will need to provide a C shim function for your C++ code.
Supporting Unregister Functionality
Supporting an unregister_callback API with a std::vector can be easy or hard depending on how you store the function target.
If you are using raw function pointers (perhaps you shouldn’t), then simply iterating through the std::vector and comparing the function pointers will suffice. To remove the element, call .erase() on the matching std::vector iterator.
If you are going to use C++11’s std::function as we described above, you will need to use the “handle method”:
- When registering a callback, store a unique number with the callback type in your
std::vector - Return this handle to the caller
- When unregistering, the caller must use the handle. Iterate through the vector until your handles match, then call
erase()on thestd::vectoriterator
Here’s an example storage type. Your register function would look roughly the same but return a uint32_t that is used to look up the std::functionobject later.
struct cb_storage_t {
std::function<void(uint32_t)> cb;
uint32_t handle;
};
std::vector<cb_storage_t> callbacks_;
Putting It All Together
I’ve added a callbacks.cpp example to the embedded-resources git repository. You can find it here.
Make sure you compile with -std=c++11 or higher set!
Further Reading
- Calllback Types
- Using a C++ Object’s Member Function with C-Style Callbacks
- C++
- Embedded Template Library
- SG14’s
inplace_function.h, which I have used on numerous Embedded Artistry projects - The Impossibly Fast C++ Delegates, Fixed
Using C++ Without the Heap
Want to use C++, but worried about how much it relies on dynamic memory allocations? Our course provides a hands-on approach for learning a diverse set of patterns, tools, and techniques for writing C++ code that never uses the heap.
Learn More on the Course Page

It’s notable that there’s a trade off in that std::function has a 4x overhead on size over a function pointer: https://godbolt.org/g/D3NnsQ
Hey Shane,
Thanks for pointing that out, I did not think to include the size overhead in the original article. Additionally, there is additional function call overhead when using std::function.
The overhead is the same order of magnitude that is incurred by using other C++ features such as smart pointers. In my opinion, the overhead is still of negligible size on most modern systems, even embedded ones.
Raw function pointers will work for most cases and are perfectly valid. std::function enables capturing additional callback types, such as lambdas with capture variables and std::bind calls.
Nice explaining, this was just what I needed! Thanks!
Glad it was useful!
Useful if you have a heap, but many embedded systems don’t have dynamic memory and thus using std::vector is impossible.
std::vector is just an example, the technique can be extrapolated to other containers. std::array works just as well without heap allocation. We also utilize the Embedded Template Library for static memory container alternatives: https://embeddedartistry.com/blog/2018/12/13/embedded-template-library.
For a non-heap alternative to std::function (the real troublesome item in this example), we use SG14’s proposed inplace_function: https://github.com/WG21-SG14/SG14/blob/master/SG14/inplace_function.h
It’s on my list to update the article with these alternatives for embedded applications without dynamic memory allocation schemes.
Thanks for adding clarity and beauty to an otherwise terse, dense and sometimes convoluted (without value) subject, such as embedded system design.
(I image this company will be very successful).
The problem with std::function is that using functional objects of larger size (eg lambdas with a lot of captured variables) will lead to dynamic allocation. There is currently no way to control this and it is implementation-defined when a lambda is permitted to allocate or not. For this reason, bare-metal projects with banned dynamic allocation should most likely write their own std::function-like wrapper that is non-owning and can be very efficiently implemented to a degree that even the extra overhead is completely eliminated (check http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0792r0.html)
Indeed.I use inplace_function from SG14 for such purposes. I’ll update the article to mention this explicitly.
https://github.com/WG21-SG14/SG14/blob/master/SG14/inplace_function.h
I’ve just recently added ‘etl::delegate’ to the Embedded Template Library, based on the CodeProject article.
https://www.codeproject.com/Articles/1170503/The-Impossibly-Fast-Cplusplus-Delegates-Fixed
I have extended it to allow delegates to be created where the object instance address can be determined at compile time, resulting in a very low call overhead.
https://www.etlcpp.com/delegate.html
This is a very nice write up. Might consider adding an example to the ‘Instance member functions’ chapter which shows how to use lambdas instead of the std:bind. I.e. something like
Client c;
register_callback([&c](uint32_t p){c->func(p);})
which I think is nicer to read than the bind variant. Works also from within a class by replacing &c with this, which is the main use case for me.
Great idea, that will be added in the next round of updates.
Hi, thanks for the article Phillip.
For me it’s a bit unclear what’s the function prototype for “register_callbac(…??…)” when registering a lambda function, I mean this:
Thank you
register_callback(
[](uint32_t v) {
printf(“lambda callback: %u\n”, v);
});
Hi Tano,
There are a number of options. Easiest is to specify the parameter as a std::function or a static-memory equivalent, such as done here.
If the lambda does not capture, it may be passed to an input parameter that expects a function pointer.
You can also use a template parameter, although you would need to execute that lambda immediately rather than storing it for later.
What about std::bind? what do I do if project won’t allow it as well? Does the libraries have something to replace it?
Hi Shanti,
Why does the project not allow std::bind? Usually the problem is that the std::function requires an allocation, and you can use a static memory implementation (like etl::delegate).
Anyway, aside from using std::bind, you could create a lambda instead, such as what luni64 described in his comment above.
I see that the article and some comments mention etl::delegate as an alternative for callbacks. The downside of etl::delegate is that it is non-owning: if a lambda is used as callback. the closure must still be alive when the callback is invoked. SG14::inplace_function (also mentioned by the writer) does not have that drawback and has the option of controlling how large the inplace_function (thus closure) is allowed to become.