Skip to content

Instantly share code, notes, and snippets.

@jancumps
Last active March 9, 2025 13:23
Show Gist options
  • Select an option

  • Save jancumps/6dbc257c80911270775ee9d44a5131ee to your computer and use it in GitHub Desktop.

Select an option

Save jancumps/6dbc257c80911270775ee9d44a5131ee to your computer and use it in GitHub Desktop.
C++ callback manager class with templated handler parameters
C++ callback manager class with templated handler parameters
check readme.MD
now in its own repository https://github.com/jancumps/callbackmanager
the gists below support running on Arduino (except AVR), or a GCC toolchain lower than 14.2
in other cases, use the repository code instead.

now in its own repository

This callback manager can execute a method of a particular object as a callback. It can be used in typical callback situations, e.g.: in a UART driver. But also in interrupt handlers.

  • lightweight: small runtime cost, low firmware hit. Can be used for tiny embedded systems.
  • reusable: don't assume parameters and types for the handler, in this little framework -> C++ templates
  • object oriented, modern C++ constructs -> std::function, lambda, again C++ templates
  • type-safe -> again C++ templates
  • allow a classic C function, a lambda, a static class method or an object method as handler.
  • pass handler arguments by value, by reference, as const

Raspberry Pico

For Raspberry Pico C/C++ SDK users, I added a CMake file. You can put that, and the header file, in a subdir of your SDK project. Then register it as a library in your project CMake file:

add_subdirectory(callbackmanager)
# ...
target_link_libraries(${CMAKE_PROJECT_NAME} pico_stdlib callbackmanager)

Arduino

For Arduino, you can download callbackmanager.h to your computer, then use Sketch -> Add File ... to get the callbackmanager included to your project. Add this line to your sketch:

#include "callbackmanager.h"

Example sketch that tests the callback manager: test_arduino.ino
Arduinos that use the gcc-avg toolchain are not supported (UNO, Mega, the original basic Nano)

Blog:

see article on element14

and a follow-up that shows different callback options (object method, static class method, C function, lambda)

/*
* callbackmanager.h
*
* Created on: 16 jun. 2024
* Author: jancu
*/
#ifndef CALLBACKMANAGER_H_
#define CALLBACKMANAGER_H_
#include <functional>
template <typename R, typename... Args>
// restrict to arithmetic data types for return value, or void
#ifdef __GNUC__ // this requires a recent version of GCC.
#if __GNUC_PREREQ(10,0)
requires std::is_void<R>::value || std::is_arithmetic_v<R> || std::is_class_v<R>
#endif
#endif
class Callback {
using callbackfunction_t = std::function<R(Args...)>;
public:
Callback() : callback_(nullptr), isSet(false){}
inline void set(callbackfunction_t callback) {
callback_ = callback;
isSet = true;
}
inline void unset() {
callback_ = nullptr;
isSet = false;
}
[[deprecated("Use operator () instead.")]]
inline R call(Args... args) {
return *this(args...);
}
inline bool is_set() {
return is_callback_set;
}
/*
* R can be an arithmetic type, an object, or void
*/
inline R operator()(Args... args) {
if constexpr (std::is_void<R>::value) { // void
if (!is_callback_set) {
return;
} else {
(callback_)(args...);
}
} else if constexpr (std::is_class<R>::value) { // object
if (!is_callback_set) {
return R();
} else {
return (callback_)(args...);
}
} else { // not void nor object
if (!is_callback_set) {
return 0; // R can only be a arithmetic type. 0 should work as default.
} else {
return (callback_)(args...);
}
}
}
private:
callbackfunction_t callback_;
bool isSet;
};
#endif /* CALLBACKMANAGER_H_ */
// https://community.element14.com/products/devtools/software/f/forum/54719/c-callbacks-and-templates
#include <cstdio>
#include "callbackmanager.h"
#include <string>
class MyClass {
public:
inline int handler(const int& num1, const int& num2) const {
return num1 + num2;
}
static inline int staticHandler(const int& num1, const int& num2) {
return num1 + num2;
}
};
int functionHandler(const int& num1, const int& num2) {
return num1 + num2;
}
int main() {
int a = 4;
int b = 5;
{ // scenario: call object method
MyClass myClass;
callbackmanager::Callback<int, const int&, const int&> cb;
// Use a lambda to capture myClass and call the object method
cb.set([&myClass](const int& num1, const int& num2) -> int const {
return myClass.handler(num1, num2);
});
int o = cb(a, b);
printf("Value: %i\n", o);
fflush(stdout);
}
{ // scenario: call static method
callbackmanager::Callback<int, const int&, const int&> cb;
// Use a lambda to call the static method
cb.set([](const int& num1, const int& num2) -> int {
return MyClass::staticHandler(num1, num2);
});
int o = cb(a, b);
printf("Value: %i\n", o);
fflush(stdout);
}
{ // scenario: call C function
callbackmanager::Callback<int, const int&, const int&> cb;
// Use a lambda to call the classic C function
cb.set([](const int& num1, const int& num2) -> int {
return functionHandler(num1, num2);
});
int o = cb(a, b);
printf("Value: %i\n", o);
fflush(stdout);
}
{ // scenario: call pure lambda
callbackmanager::Callback<int, const int&, const int&> cb;
// Use a lambda to execute anonymous C code
cb.set([](const int& num1, const int& num2) -> int {
return num1 + num2;
});
int o = cb(a, b);
printf("Value: %i\n", o);
fflush(stdout);
}
{ // scenario: return a bool
callbackmanager::Callback<bool, const int&, const int&> cb;
// Use a lambda to execute anonymous C code
cb.set([](const int& num1, const int& num2) -> bool {
return num1 == num2;
});
printf("Value: %s\n", cb(a, b) ? "true" : "false");
fflush(stdout);
}
{ // scenario: use void
callbackmanager::Callback<void, const int&, const int&> cb;
// Use a lambda to execute anonymous C code
cb.set([](const int& num1, const int& num2) {
printf("void gets num1: %i, num2: %i\n", num1, num2);
fflush(stdout);
return;
});
cb(a, b);
}
{ // scenario: use void, and no attributes
callbackmanager::Callback<void> cb;
// Use a lambda to execute anonymous C code
cb.set([]() {
printf("void with no parameters\n");
fflush(stdout);
return;
});
cb();
}
{ // scenario: use void, and a const std::string reference
callbackmanager::Callback<void, const std::string&> cb;
// Use a lambda to execute anonymous C code
cb.set([](const std::string& s) {
printf("%s", s.c_str());
fflush(stdout);
return;
});
cb("hello, world!\r\n");
}
{ // scenario: use void, and a const std::string reference
std::string updateme;
callbackmanager::Callback<void, std::string&> cb;
// Use a lambda to execute anonymous C code
cb.set([](std::string& s) {
s.assign("hello, world!\r\n");
return;
});
cb(updateme);
printf("%s", updateme.c_str());
fflush(stdout);
}
{ // scenario: return an object
class return_class {
public:
return_class() : value_(0) {};
int value_;
};
callbackmanager::Callback<return_class> cb;
// Use a lambda to execute anonymous C code
cb.set([]() -> return_class {
return_class rc;
rc.value_ = 1;
return rc;
});
return_class ret = cb();
printf("Value: %d\n", ret.value_ );
fflush(stdout);
}
/*
{ // scenario: use an unsupported (non-fundamental) type for return value R
// this will generate a compile error
callbackmanager::Callback<std::string, const int&, const int&> cb;
}
*/
}
#include <cstdio>
#include <string>
#include "callbackmanager.h"
class MyClass {
public:
int handler(const int& num1, std::string& s) {
printf("Message: %s\n", s.c_str()); // We might be on an embedded system, use printf() and not std::cout
fflush(stdout);
return num1;
}
};
int main() {
MyClass myClass;
Callback<int, const int&, std::string&> cb;
// Use a lambda to capture myClass and call the member method
cb.set([&myClass](const int& num1, std::string& s) -> int {
return myClass.handler(num1, s);
});
int a = 4;
std::string s = "hey";
int o = cb(a, s);
printf("Value: %i\n", o); // We might be on an embedded system, use printf() and not std::cout
fflush(stdout);
}
# for Raspberry Pico / RP2040
# in the repository https://github.com/jancumps/callbackmanager
# there is now a simpler way to integrate, using C++ modules.
# if you are running on a 14.2 or higher GCC toolchain, use that
add_library( callbackmanager
callbackmanager.h
)
# solve CMake can not determine linker language for target
# because the module has no source file
set_target_properties(callbackmanager PROPERTIES LINKER_LANGUAGE CXX)
target_link_libraries(callbackmanager
)
target_include_directories(callbackmanager PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
#include "callbackmanager.h"
// https://community.element14.com/products/devtools/software/f/forum/54719/c-callbacks-and-templates
#include <cstdio>
#include "callbackmanager.h"
#include <string>
class MyClass {
public:
inline int handler(const int& num1, const int& num2) const {
Serial.println("class member function callback ");
return num1 + num2;
}
static inline int staticHandler(const int& num1, const int& num2) {
Serial.println("static class member function callback ");
return num1 + num2;
}
};
int functionHandler(const int& num1, const int& num2) {
Serial.println("classic C function callback ");
return num1 + num2;
}
void setup() {
char msg[50];
Serial.begin(9600);
delay(4000); // enough time to open the Serial Monitor :)
Serial.println("starting up");
int a = 4;
int b = 5;
{ // scenario: call object method
MyClass myClass;
Callback<int, const int&, const int&> cb;
// Use a lambda to capture myClass and call the object method
cb.set([&myClass](const int& num1, const int& num2) -> int {
return myClass.handler(num1, num2);
});
int o = cb(a, b);
sprintf(msg, "Value: %i\n", o);
Serial.print(msg);
}
{ // scenario: call static method
Callback<int, const int&, const int&> cb;
// Use a lambda to call the static method
cb.set([](const int& num1, const int& num2) -> int {
return MyClass::staticHandler(num1, num2);
});
int o = cb(a, b);
sprintf(msg, "Value: %i\n", o);
Serial.print(msg);
}
{ // scenario: call C function
Callback<int, const int&, const int&> cb;
// Use a lambda to call the classic C function
cb.set([](const int& num1, const int& num2) -> int {
return functionHandler(num1, num2);
});
int o = cb(a, b);
sprintf(msg, "Value: %i\n", o);
Serial.print(msg);
}
{ // scenario: call pure lambda
Callback<int, const int&, const int&> cb;
// Use a lambda to execute anonymous C code
cb.set([](const int& num1, const int& num2) -> int {
Serial.println("lambda int function callback ");
return num1 + num2;
});
int o = cb(a, b);
sprintf(msg, "Value: %i\n", o);
Serial.print(msg);
}
{ // scenario: return a bool
Callback<bool, const int&, const int&> cb;
// Use a lambda to execute anonymous C code
cb.set([&msg](const int& num1, const int& num2) -> bool {
Serial.println("lambda bool function callback ");
return num1 == num2;
});
sprintf(msg, "Value: %s\n", cb(a, b) ? "true" : "false");
Serial.print(msg);
}
{ // scenario: use void
Callback<void, const int&, const int&> cb;
// Use a lambda to execute anonymous C code
cb.set([&msg](const int& num1, const int& num2) {
Serial.println("lambda void function callback ");
sprintf(msg, "Value a: %i, value b: %i\n", num1, num2);
Serial.print(msg);
return;
});
cb(a, b);
}
{ // scenario: use void, and no attributes
Callback<void> cb;
// Use a lambda to execute anonymous C code
cb.set([]() {
Serial.println("lambda void function with no parameters callback ");
Serial.println("hello ");
return;
});
cb();
}
/*
{ // scenario: use an unsupported (non-fundamental) type for return value R
// this will generate a compile error
Callback<std::string, const int&, const int&> cb;
}
*/
}
#include <limits.h>
void loop() {
delay(ULONG_MAX);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment