I'm writing a callback wrapper class for an embedded application in C++11.
The basic idea of the class is to be able to use it as a replacement instead of C-style callback functions, with the added benefit of being able to wrap functors and lambdas.
Requirements
- No dynamic memory allocations are allowed at all.
- It only needs to handle small functors (and lambdas).
- Return type of the wrapped functions is always
void
. - It should be able to wrap C-style callback functions too.
Some implementation details
Basically, the class holds a function pointer with an optional user-specified parameter that is passed in as the first argument to the function pointer. It also has a small statically-allocated byte array in order to be able to wrap small functors and lambdas.
The wrapping is done in a templated operator=
function (and a constructor) overload which simply copies the state of the functor (or lambda), and then sets the function pointer to a function that calls it.
The code
Here is the implementation of the class.
I also wrote some Doxygen-style API docs, hope it's okay to keep them intact here.
/**
* Callback is a functor class that helps wrapping callback functions.
*
* It stores a function pointer and an optional user-specified parameter. The
* function pointer has a signature that takes the user-specified parameter first,
* and then the specified argument types.
*
* It also provides a convenient way to wrap C++ lambda functions, even capturing
* lambdas into a callback.
*
* Template parameters of this class are the argument types of the callback.
*/
template <typename ... TArgs>
class Callback final {
public:
/**
* The function pointer type that is stored in this class.
*/
typedef void (*Function)(void *, TArgs ...);
/**
* The function pointer type without the user parameter that is stored in this class.
*/
typedef void (*SimpleFunction)(TArgs ...);
private:
/**
* The function pointer that is executed by the current Callback instance. Default is nullptr.
*/
Function func_;
/**
* The user pointer that is passed as the first argument to the function pointer.
*/
void *userPtr_;
/**
* If the callback is a wrapped functor (eg. a capturing lambda function), its data is stored here.
*/
uint8_t functorData_[16];
public:
/**
* Initializes an empty Callback instance.
*/
explicit inline Callback() : func_(nullptr), userPtr_(nullptr) {
this->operator=(nullptr);
}
/**
* Copy constructor that copies the state of another Callback instance.
*/
inline Callback(const Callback<TArgs...> &other) : Callback() {
this->operator=(other);
}
/**
* Initializes a Callback instance from a functor object (eg. a capturing lambda function).
*/
template <typename TFunctor>
inline Callback(const TFunctor &functor) : Callback() {
this->operator=(functor);
}
/**
* Sets the callback function and user pointer.
*/
inline Callback &operator=(const ::std::tuple<Function, void*> &stuff) {
this->func_ = stuff.first;
this->userPtr_ = stuff.second;
::std::memset(this->functorData_, 0, sizeof(this->functorData_));
return *this;
}
/**
* Sets the callback function and user pointer by wrapping
* a functor object (eg. a capturing lambda function).
*/
template <typename TFunctor>
inline Callback &operator=(const TFunctor &functor) {
// Compile-time check to see if the passed functor can be held by the Callback class
static_assert(::std::is_assignable<::std::function<void(TArgs...)>, TFunctor>::value, "Invalid functor object passed to Callback.");
static_assert(sizeof(functorData_) >= sizeof(TFunctor), "Functor object is too big.");
// Copy the functor
::std::memcpy(this->functorData_, &functor, sizeof(TFunctor));
// Set the user pointer to the copied functor
this->userPtr_ = (void*) this->functorData_;
// Create a simple function that calls the functor
this->func_ = [](void *user, TArgs ... args) -> void {
TFunctor *functorPtr = reinterpret_cast<TFunctor*>(user);
(*functorPtr)(args...);
};
return *this;
}
/**
* Sets the callback function and user pointer by wrapping
* a simple C function pointer (without the user parameter).
*/
inline Callback &operator=(SimpleFunction function) {
this->operator=([function] (TArgs ... args) -> void {
function(args...);
});
return *this;
}
/**
* Clears the Callback object.
*/
inline Callback &operator=(nullptr_t) {
this->func_ = nullptr;
this->userPtr_ = nullptr;
::std::memset(this->functorData_, 0, sizeof(this->functorData_));
return *this;
}
/**
* Copies the state of another Callback instance.
*/
inline Callback &operator=(const Callback<TArgs...> &other) {
this->func_ = other.func_;
this->userPtr_ = other.userPtr_;
::std::memcpy(this->functorData_, other.functorData_, sizeof(this->functorData_));
return *this;
}
/**
* Executes the callback function. If the Callback is empty,
* it simply does nothing. No exception is thrown.
*
* The specified user pointer is passed as the first argument
* to the callback function.
*/
inline void operator()(TArgs ... args) {
if (this->func_ != nullptr) {
this->func_(this->userPtr_, args ...);
}
}
/**
* Compares the current Callback instance to a null pointer,
* meaning that it returns true if the Callback is empty.
*/
inline bool operator==(nullptr_t) {
return nullptr == this->func_;
}
/**
* Compares the current Callback instance to a null pointer,
* meaning that it returns true if the Callback is NOT empty.
*/
inline bool operator!=(nullptr_t) {
return nullptr != this->func_;
}
/**
* Compares two Callback instances and returns true if they
* are equal.
*/
inline bool operator==(const Callback &other) {
return (this->func_ == other.func_) &&
(this->userPtr_ == other.userPtr_) &&
(0 == ::std::memcmp(this->functorData_, other.functorData_, sizeof(this->functorData_)));
}
/**
* Converts the Callback into a boolean value.
* Returns false if the Callback is empty, true otherwise.
*/
inline operator bool() {
return this->operator !=(nullptr);
}
};
Intended use
Let's assume you have an MCU (microcontroller unit) and have a SerialPort
class which knows how to send and receive data (ie. it manipulates the registers of the MCU to control the port).
class SerialPort {
// ... etc.
public:
virtual void startTransmit(const void *buffer, uint32_t length) = 0;
virtual void startReceive(void *buffer, uint32_t length) = 0;
// ...
};
This is cool, but it would be nice if this class could tell you what it received, wouldn't it?
Now you could do this by adding a function pointer:
private:
void (*receiveCallback)(const void *buffer, uint32_t length);
public:
inline void setReceiveDoneCallback(const decltype(receiveCallback) &cb) {
this->receiveDoneCallback = cb;
}
Then you „only” have to call the receiveCallback
whenever the port has received some data. (For example, from an interrupt service routine, but let's omit the hardware-specific code now for brevity.)
However, this C-style callback mechanism has some serious drawbacks, most notably the fact that using this scheme it is impossible to tell anything about the context of the callback site. Assuming that multiple instances of the SerialPort
can exist, C-style callbacks are very hard to use from object-oriented code.
Better C libraries extend the above scheme with a void*
„user pointer” that is passed to the called function, making the life of everyone easier. It still remains inconvenient because you can't assign capturing lambdas or functors.
Let's now use the Callback
class instead of a function pointer:
private:
Callback<const uint8_t*, uint32_t> receiveDoneCallback;
public:
template <typename T>
inline void setReceiveDoneCallback(const T &cb) {
this->receiveDoneCallback = cb;
}
Then the usage of the SerialPort
becomes easier:
SerialPort port (...);
// Hardware-specific setup omitted for brevity...
port.setReceiveDoneCallback([&](const void* buffer, uint32_t length) -> void {
// You can use anything from the context, have access to the `this` ponter, etc.
});
Other notes
- This code will run on a bare-metal microcontroller, without an operating system.
- I'm somewhat afraid to “just use”
std::function
for this purpose because it uses dynamic memory allocations (and its implementation looks scarily complicated). - (I've also seen other implementations of the same concept on Code Review, but none of those seem suitable for my use case.)