-
Notifications
You must be signed in to change notification settings - Fork 5
Better Callbacks
This tutorial demonstrates how to use std::function<>
instead of the usual function pointer approach to implement a callback mechanism.
- Traditional Callbacks
- Using std::function instead of function pointers
- Improved Frequency Generator
The usual way to provide a callback mechanism is to store the address of a user supplied callback function in a member variable of the callback provider. The provider can then invoke the callback by simply calling it using the stored address. While this works of course, it has some nasty drawbacks. E.g., passing context, or using a member function as a callback can only be achieved by some ugly workarounds.
In the course of this tutorial we will use the following simple software timer as an example of a class providing a callback mechanism.
Here the class declaration. Complete sketch can be found here.
#pragma once
#include "Arduino.h"
using callback_t = void (*)(void); // c++ way to define a function pointer (since c++11)
//typedef void (*callback_t)(void); // traditional C typedef way works as well
class SimpleTimer
{
public:
void begin(unsigned period, callback_t callback);
void tick(); // call this as often as possible
protected:
unsigned period;
elapsedMicros timer;
callback_t callback;
};
Class definition:
#include "SimpleTimer.h"
void SimpleTimer::begin(unsigned _period, callback_t _callback)
{
period = _period;
callback = _callback;
timer = 0;
}
void SimpleTimer::tick()
{
if (timer >= period)
{
timer -= period;
callback();
}
}
How does it work?
-
The
begin
function takes and stores the timer period and the callback function and resets the elapsedMicros based timer variable. -
The
tick()
function does the actual timing. You should call it as often as possible. It simply checks if the elapsed time is larger than the timer period and, in this case, invokes the callback function and resets the timer variable.
You use the timer class as shown below. Here we define the callback function onTimer()
and attach it to the timer which will call it every 100ms.
#include "SimpleTimer.h"
SimpleTimer timer;
void setup()
{
timer.begin(100'000, onTimer);
}
void loop()
{
timer.tick(); // update
}
void onTimer() // callback Function
{
Serial.printf("Called at %d ms\n", millis());
}
Which will print out:
Called at 500 ms
Called at 600 ms
Called at 700 ms
Called at 800 ms
Called at 900 ms
...
(Complete sketch can be downloaded here)
Now, let's use our SimpleTimer class to implement a frequency generator. Of course the timer, which will generate the output signal, can be regarded as implementation detail. Thus, we try to hide it away from the user and encapsulate it in the frequency generator class.
Here the declaration (goes into FrequencyGenerator.h)
#pragma once
#include "SimpleTimer.h"
class FrequencyGenerator
{
public:
FrequencyGenerator(unsigned pin);
void setFrequency(float Hz);
protected:
SimpleTimer timer;
unsigned pin;
void onTimer();
};
and the definition (goes into FrequencyGenerator.cpp)
#include "FrequencyGenerator.h"
FrequencyGenerator::FrequencyGenerator(unsigned _pin)
{
pin = _pin;
pinMode(pin, OUTPUT);
}
void FrequencyGenerator::setFrequency(float frequency)
{
unsigned period = 0.5f * 1E6f / frequency;
timer.begin(period, onTimer); // attach callback
}
void FrequencyGenerator::onTimer()
{
digitalWriteFast(pin, !digitalReadFast(pin));
}
The idea of the implementation is to define a member function onTimer()
which simply toggles the output pin and attach this function as a callback to the timer in setFrequency(float)
.
Unfortunately, the compiler doesn't like our smart idea at all and complains that we try to pass the address of a non-static member function to timer.begin
, which expects an address of a void function returning void;
FrequencyGenerator.cpp: In member function 'void FrequencyGenerator::setFrequency(float)':
FrequencyGenerator.cpp:12:32: error: invalid use of non-static member function
timer.begin(period, onTimer); // attach callback
^
The root cause for this compiler error is, that all non static member functions carry an implicit, invisible, compiler generated parameter which is needed to identify the actual object the member function belongs to. Thus, the signature of the onTimer
function doesn't fit to the required type (void function returning void). We could of course fix this by making onTimer()
static. But, this would make it impossible to use more than one FrequencyGenerator at the same time, since all objects would share the same static onTimer()
callback.
Opposed to a simple function pointer a std::function accepts pretty much any object which can be called. (If you are interested in more details there is a nice writeup on stackoverflow Should I use std::function or a function pointer in C++? )
So, let's improve our timer example by using a std::function<>. Actually, all we have to do is to exchange the type of the callback_t alias at the top of SimpleTimer.h from a function pointer to a std::function<>
:
...
#include <functional>
using callback_t = std::function<void(void)>; // define a "void foo(void)" typed std::function
//using callback_t = void (*)(void); // function pointer
class SimpleTimer
{
public:
...
Before implementing this from the FrequencyGenerator class, here some examples what we can do with our new toy.
TBD
TBD
TBD
Work in progress...
Teensy is a PJRC trademark. Notes here are for reference and will typically refer to the ARM variants unless noted.