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.)
-
2\$\begingroup\$ I have scribbled up a compiling version based on the OP's example here: coliru.stacked-crooked.com/a/cc9bf82729c58fa1 \$\endgroup\$πάντα ῥεῖ– πάντα ῥεῖ2017年01月08日 12:50:04 +00:00Commented Jan 8, 2017 at 12:50
-
\$\begingroup\$ codeproject.com/articles/11015/the-impossibly-fast-c-delegates <<--- I think I used this on embedded systems for a while (V850, Greenhills compiler (OSEK, Bare Metal), FreeBSD, Linux, Windows 32Bit/64Bit, Windows CE 6.0 on Freescale and TI MCUs) and it was working. Benefit: No heap allocations in the implementation. But as usual, handle with care and do your own tests to make sure all is good. \$\endgroup\$BitTickler– BitTickler2017年01月08日 14:50:46 +00:00Commented Jan 8, 2017 at 14:50
1 Answer 1
A few thoughts about your code:
1. Use of reinterpret_cast
// Create a simple function that calls the functor
this->func_ = [](void *user, TArgs ... args) -> void {
TFunctor *functorPtr = reinterpret_cast<TFunctor*>(user);
(*functorPtr)(args...);
};
This always raises a red flag for me. I understand that the user
parameter is used to pass the actual functor instance, and it's a common technique to interface with C-style callback API's.
May be it deserves more commenting why exactly it will be used here, and what exactly user
is expected to carry.
2. Using a fixed size for the functorData_
member
This makes your class less flexible for generic usage.
You could use an additional template parameter to specify the size
template <size_t MaxFunctorSize, typename ... TArgs>
class Callback final {
// ...
uint8_t functorData_[MaxFunctorSize];
};
and use that with the static_assert
s etc.
At least the callback holder could decide then, how many stack storage should me reserved as maximum.
The downside is, that the maximum size cannot be defaulted to a standard value because of the variadic parameter pack must appear as the last parameter.
3. Wasting memory for sake of making client code easier to write
I'm not a 100% sure that wasting (stack) space is worth that (especially when dealing with a small MCU at the bare metal level).
May be the design could be improved, and less memory intrusive with bit a more complicated SFINAE based class model to handle the different cases
- Simple C-Style function pointer
- Functor object / Lambda expression
4. Potential slicing problems with functor instances
I smell potential slicing problems with virtual polymorphic functor class hierarchies.
The functor interface using functorData_
may fail to work properly with virtual inheritance:
struct IFunctor {
virtual void operator()(const uint8_t*, uint32_t) = 0;
virtual ~IFunctor() = default;
};
struct AFunctor : IFunctor {
virtual void operator()(const uint8_t*, uint32_t) {
// Do something
}
};
void doSomething() {
SerialPort port;
AFunctor aFunc;
IFunctor* iFunc = &aFunc;
port.setReceiveDoneCallback(*iFunc);
}
You can avoid that adding another static_assert
:
static_assert(!std::is_polymorphic<TFunctor>(),"Functor object must not be polymorphic.");
Note that this is related to my points 1. and 2.
5. The use of inline
is superfluous inside the class declaration body
As mentioned above, you don't even need to use inline
inside of the class body declaration. That's merely a hint for the compiler, and will be done (or overridden) regarding the chosen optimization strategy anyways.
The only case you must use the inline
keyword is, if you're defining global functions in a multiply included header file. Otherwise you are going to violate the One Definition Rule principle.
Regarding GCC's behavior, it's documented here (emphasis mine):
GCC does not inline any functions when not optimizing unless you specify the ‘always_inline’ attribute for the function, like this:
/* Prototype. */ inline void foo (const char) __attribute__((always_inline));
Otherwise, it depends on optimization settings.
6. The use of final
is superfluous
Since your class doesn't expose any virtual
functions that could be potentially overridden by inheriting classes, you simply can omit it.
See also: In C++, when should I use final in virtual method declaration?
-
\$\begingroup\$
inline
is not related to optimization at all, not even is this a hint (at least per the Standard). It's instead a means of avoiding ODR violations. \$\endgroup\$Ruslan– Ruslan2017年01月08日 15:03:41 +00:00Commented Jan 8, 2017 at 15:03 -
\$\begingroup\$ @Ruslan I mentioned that clearly IMO? \$\endgroup\$πάντα ῥεῖ– πάντα ῥεῖ2017年01月08日 15:04:30 +00:00Commented Jan 8, 2017 at 15:04
-
\$\begingroup\$ You say "That's merely a hint for the compiler". It's not. Either you violate ODR, or you don't. \$\endgroup\$Ruslan– Ruslan2017年01月08日 15:05:00 +00:00Commented Jan 8, 2017 at 15:05
-
1\$\begingroup\$ @Ruslan As whatever hint the compiler is willing to take it. What's the concrete point of your nitpick? Do you have an actual proposal for improving the statement? \$\endgroup\$πάντα ῥεῖ– πάντα ῥεῖ2017年01月08日 15:08:19 +00:00Commented Jan 8, 2017 at 15:08
-
1\$\begingroup\$ Your overly-pedantic comments ignore the realities of the world, @ruslan. There are compilers for which the
inline
keyword is a hint to actually inline the definition of the function in the caller. Clang and MSVC are among them. You can verify this for yourself with Clang, since it is open source. Naturally, as πάντα said, it's just a hint, and the optimizer will make the final decision, but it is a hint that is taken into account. And the language standard does not forbid that. That said, I agree that its primary purpose (and the one you should think of) is to avoid ODR violations. \$\endgroup\$Cody Gray– Cody Gray2017年01月08日 17:20:43 +00:00Commented Jan 8, 2017 at 17:20
Explore related questions
See similar questions with these tags.