element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Software
  • Products
  • Dev Tools
  • Software
  • More
  • Cancel
Software
Forum C++ callbacks and templates
  • Forum
  • Documents
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Software to participate - click to join for free!
Actions
  • Share
  • More
  • Cancel
Forum Thread Details
  • Replies 17 replies
  • Subscribers 26 subscribers
  • Views 1937 views
  • Users 0 members are here
  • isr
  • interrupt
  • stl
  • OO
  • c++
  • callback
Related

C++ callbacks and templates

Jan Cumps
Jan Cumps over 1 year ago

I'm experimenting: can I use a method of a particular object as a callback? This 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, when writing this little framework -> C++ templates
  • hip -> object oriented, modern C++ constructs
  • type safe -> again C++ templates
  • allow a classic C function, a lambda, a static class method or an object method as handler

example: Raspberry Pico blinky using this design

#include <new>
#include <stdio.h>
#include "pico/stdlib.h"

import callbackmanager;

const uint32_t delay_ms = 250;

callbackmanager::Callback<void> cb;

int main() {
    gpio_init(PICO_DEFAULT_LED_PIN);
    gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
    cb.set([]() {
        static bool state = false;
        gpio_put(PICO_DEFAULT_LED_PIN, state = !state);
    });   

    while (true) {
        cb();
        sleep_ms(delay_ms);
    }
}

project: 2133.pico_callback_blink.zip

Callback template class

The template callback class. This is in essence the whole mechanism. A generic class that allows you to register and call event handler functions, lambda's and methods.

#include <functional>

// concept guards what types of return values we can handle
template<typename R>
concept Callbackable = 
	std::is_void<R>::value ||
	std::is_arithmetic_v<R> ||
	std::is_class_v<R>;
	
template <Callbackable R, typename... Args>
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;
	}

	inline bool is_set() {
		return isSet;		
	}

	/*
	 * 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;
};

This generic class allows for handlers that pass any amount of parameters. No assumption of the types.

When we use the class in our code, we 'll tell it what the parameters are, and their type. For this blog, I am going to pass it 2 integers. The return value is also an integer.

Callback<int, int, int> cb;

In the declaration above, the first int is the return value type, the 2 others are parameter 1 and 2. Once you write this code, the object cb will only accept handler methods that take two integers as parameter, and return an integer.

Example of class that will handle the callback

This could be a gio class (blinky a led when an interrupt occurred?), a GUIwidget, or a business object. Typical guidelines apply: a handler should be swift. In particular if it's an interrupt handler.

class MyClass {
public:
      int handler(int num1, int num2) {
          return num1 + num2;
      }
};

The example does not do a lot today. It just sums up the two integers passed. But as a POC this will do.

Actual code using the two

#include <cstdio>

int main()
{
    MyClass myClass;

    Callback<int, int, int> cb;
    // Use a lambda to capture myClass and call the member method
    cb.set([&myClass](int num1, int num2) -> int {
        return myClass.handler(num1, num2);
    });

    int a = 4;
    int b = 5;

    int o = cb(a, b);
    printf("Value: %i\n", o); // We might be on an embedded system, use printf() and not std::cout
}

Why use a lambda to pair the handler and Callback ?

In the code, I did not learn the MyClass how to register itself. And the Callback doesn't know anything about who or what it 'll call. These classes (and their objects) are unaware of each other. Loose coupling.

Callback<int, int, int> cb;

cb.set([&myClass](int num1, int num2) -> int {
  return myClass.handler(num1, num2);
})
;

In the application logic, I tie the two together. The lambda function code (the anonymous function, starting with the [, and ending with the } above), is used as the vehicle to hold the code that the Callback object will execute when needed.

The lambda function could as well be a straigtforward piece of code, not related to any object:

Callback<int, int, int> cb;

cb.set([](int num1, int num2) -> int {
  return num1 + num2;
});

Or we could pass completely different executable code. As long as the lambda matches the signature given when instantiating the Callback class.

In an embedded system,

  • myClass would be one of your application objects.
  • cb could be part of a driver, or the HAL layer
  • cb.set() would be called by you, to register the handler
  • cb.call() would be done by the driver, or HAL ISR, when it wants to notify you (call your callback handler)

In the case of this example, it will execute myClass.handler(). If you 'd have many MyClass objects, it would call the exact one that's registered in with set().

image
Example use of the callback class in an embedded application

Anything special?

This approach allows to write a reusable callback system. It is type safe. But it does not assume what the type is of the object it 'll call the handler off. (Or if it's actually an object).
It's a small amount of code, that performs a function that you regularly need in an embedded or event driven design.

What does type-safe mean in this context?

If your interrupt handler assumes a function that passes 2 integers, and you pass it a callback function that has different parameters,
you get a compile time error. Your code will not build, because the handler is not compatible.

And you get all of that, without having to specify the number of variables, and their types, in the Callback class code. We get a reusable class. Type safe. Without the need to foresee all possible combinations.

this library now also works for Arduino (tested with IDE2, and an Arduino MKT GSM 1400)

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"

I added a sketch to test the callback manager to this gist: test_arduino.ino

image
Arduinos that use the gcc-avg toolchain are not supported (UNO, Mega, the original basic Nano)

shortcuts:

  • I assume fixed return value type. A richer implementation can use template for that too, and should allow void. edit: this works
  • I pass arguments by value. A better design would also allow pass by reference. And to pass const references. edit: this works
  • should I use a shared pointer, so that I can never call the handler of an object that no longer exists? (solved by alternative: provide an unset() method). edit: I no longer use pointers
  • it only allows to call one handler. Good for ISR use. But in an event driven design, you may want to register more handlers. I prefer just one handler. simple scope, simple responsibility.

Inspiration:

  • I started from this example by Geoffrey Hunter.
  • mbed's Callback class, although I didn't use any of the code
  • cplusplus' tutorial for std::function, to validate if it works for static class member, object member, classic C function and lambda function.

Code is available on github. Additional assets available on Gist.

CMakeLists.txt to build an example program:

cmake_minimum_required(VERSION 3.28)

project(callbackmanager C CXX ASM)

# GoogleTest requires at least C++14
set(CMAKE_CXX_STANDARD 26)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmodules-ts -fcommon")

add_executable(${CMAKE_PROJECT_NAME})
target_sources(${CMAKE_PROJECT_NAME}
        PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/example/callback_examples.cpp
)
target_sources(${CMAKE_PROJECT_NAME}
        PUBLIC
        FILE_SET cxx_modules TYPE CXX_MODULES FILES
        ${CMAKE_CURRENT_SOURCE_DIR}/callbackmanager.cpp
)

target_link_libraries( ${CMAKE_PROJECT_NAME}
)

I test this code in  C++ callbacks and templates: make the Callback class call an object member, a static member, a C function and a pure lambda function 
edit 20250208: I converted it to a c++ module: modern C++ modules: convert existing example from .h to module 

  • Sign in to reply
  • Cancel

Top Replies

  • Jan Cumps
    Jan Cumps over 1 year ago +2
    The code is now available on Gist . I've put the template class in a header file. Also: tested on a Pico: I also attached a Pico SDK subproject archive, plug-and-play callback
  • shabaz
    shabaz over 1 year ago +1
    Hi Jan, Very interesting! By coincidence I too was looking for a way of doing callbacks, mine is nowhere near as clean though. I went with virtual functions, i.e.: class Bob_base { public: .. virtual…
  • Jan Cumps
    Jan Cumps over 1 year ago in reply to shabaz +1
    added advantage: you don't need inheritance. And nearly everything is resolved at compile time. Because this approach allows that you can call back methods of any class (or even functions that don't…
  • shabaz
    shabaz over 1 year ago

    Hi Jan,

    Very interesting! By coincidence I too was looking for a way of doing callbacks, mine is nowhere near as clean though. I went with virtual functions, i.e.:

    class Bob_base {
      public:
        ..
        virtual int callback(int param);
    };

    And then:

    class Bob : public Bob_base {
      int callback(int param) {
        printf("callback");
      }
    };

    The template approach is way cleaner since you don't need to create another class.

    It's awesome that C++ is powerful enough for all these techniques.

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago in reply to shabaz

    added advantage: you don't need inheritance. And nearly everything is resolved at compile time.

    Because this approach allows that you can call back methods of any class (or even functions that don't belong to a class),
    it keeps your hierarchy simpler.

    No need to write a base class that's callback aware. (although you can still do that, if you prefer it).

    This will help to make inheritance focus on core functionality. And not being diluted by utilitarian things like callback.

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • Cancel
  • shabaz
    shabaz over 1 year ago in reply to Jan Cumps

    Very good points. It is in essence abusing inheritance just for the purposes of a callback, I didn't think of that originally, but it would definitely be undesirable especially in a more complex piece of software with lots going on.

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago in reply to shabaz

    In the early days of C++ (multiple) inheritance was the only solution for it. Maintaining complex software over years, has learned that this can turn into a very complex hierarchy, In particular if you use it for, say, callbacks, printing, storing to disk, formatting, and then also for the real business logic.

    The new functionalities in C++ are driven by the learnings of using the language in anger. And by recognising where a pattern turned out to be unmanageable or resource-intensive.

    Inheritance is great. Multiple inheritance is useful (see that I didn't use the word great :) - although it has its place). But the consortium listened, and provided alternatives. Some solved problems or simplified things. Others turned out to not be that efficient. That's why we've seen some improvements stay, and others being deprecated again.

    I like the latest C++ standard: it's a stabilising one. Consolidating the new features that solved a problem. Shutting down a few that turned out not to be the magic bullet.

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago

    I expected that my code would not work with "pass by reference", and "const" parameters without rework. But it actually does.

    Here, I pass parameter 1 as a constant int reference, and parameter 2 as a string object reference. Works :). Without adapting the template class.

    #include <string>
    
    // ...
    
    
    class MyClass {
    public:
          int handler(const int& num1, std::string& s) {
              return num1;
          }
    };
    
    int main()
    {
        MyClass myClass;
    
        Callback<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.call(a, s);
        printf("Value: %i\n", o); // We might be on an embedded system, use printf() and not std::cout
    }

    pass by reference is useful in several cases:

    • the value of the variable can be changed by the callback (if you don't declare the parameter as const).
    • if it's an object, there's no copy taken. You pass the object itself and can call its members. If the parameter is declared as const, you can only call members that don't alter the object state.

    pass as const has this goal (related with the previous explanation):

    • the code has direct access to the object / variable, but can't change it. 
    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago

    The code is now available on Gist.

    I've put the template class in a header file. 

    Also: tested on a Pico:

    image

    I also attached a Pico SDK subproject archive, plug-and-play callback Slight smile

    • Cancel
    • Vote Up +2 Vote Down
    • Sign in to reply
    • Cancel
  • flyingbean
    flyingbean over 1 year ago

    Great tip. I could use the template  in my projects here.

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago in reply to flyingbean

    I've been testing it with toolchains. 

    Works with:

    • NXP MCUXpresso
    • Pico C/C++ SDK 1.5
    • Renesas e2 studio with gcc toolchain (everything, except the optional clause requires std::is_arithmetic<R>::value)
    • Eclipse with the arm linux cross-compiler (for Raspberry Pi, BB, ...)

    Does not work with:

    • Arduino IDE 2.0 avr-gcc: compiler version 7.3 is too low
    • Arduino IDE 2.0 arm-gcc: compiler version 7.2 is too low
    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago

    Most of this design only has compile time cost. The only thing that has some runtime cost is the std::function invocation. This part of code:

    (*_callback)(args...);

    There are four function calls in between this call and the actual execution of the callback function registered. Not a lot of code is executed, but four calls nonetheless.
    I'd consider C++ abstractions cheap. But if 4 calls are too much for the design you are working on, this is not your solution.
    We're not talking a lot of clock ticks though - this is all lean.

    The cheapest way will most likely be to write callback functionality in assembler or using C style (both without type safety and object awareness). On a controller like the Pico, I think that the cost is ignorable in the majority of firmware designs.
    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • Cancel
  • flyingbean
    flyingbean 11 months ago in reply to Jan Cumps

    I just got a new board from Renesas. Will try e2 studio after my vacation this Summer.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • Cancel
>
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube