I am working on an API that I want as generic as possible on the caller side. Main design idea is to provide a signal/slot sort of implementation that allows the user of the API to subscribe to a given set of events, and attach user-defined callbacks to them.
Public interface looks something like this:
RetCallback subscribe(EventEnum& ev, std::function<void(void*)> fn) const;
: note the void(void*)
signature here. EventEnum
is given in a public header file, as well as types definition.
Inner-works of the API would then notify its subscribed observers of the event through a notify method and provide data to forward to the client :
void dummyHeavyOperation() const {
std::this_thread::sleep_for(2s);
std::string data = "I am working very hard";
notify(EventEnum::FooEvent, &data);
}
Client subscribes and casts data to (documented) type as follows:
auto subscriber = Controller->subscribe(EventEnum::FooEvent, callback);
where
void callback(void* data) {
auto* myData = (std::string*) data;
std::cout << "callback() with data=" << *myData << std::endl;
/// Do things
}
Is this a reasonable design or is this frowned upon? What is your experienced modern C++ developer mind tells you?
[EDIT]
I should add as well that the API is delivered as a shared library loaded at run-time. So any compile time coupling (and code generation for that matter, unless I'm mistaken) is off the table
Thanks!
Yes, void*
is a bad idea. Even more so when the involved types come from you, not the user!
If different events pass different types data to the client, then enforcing type safety is very useful for the user. It prevents things like accidentally passing a callback that expects a string but is called with a double. You want your API to be hard to misuse.
For example, you could do this:
template<class T>
RetCallback subscribe(EventEnum& ev, std::function<void(T)> fn) const;
Subscribers would spell out the type at call site:
auto subscriber = Controller->subscribe<std::string>(EventEnum::FooEvent, callback);
You can then check in subscribe
whether the EventNum
is ok with that callback signature, or you could even (depending on how many events and callback data types you have) have different EventNum
types for each callback data type so that it is impossible to even call subscribe with mismatching event type and callback signature, like this: https://godbolt.org/g/7xTGiM
notify
would have to be done in a similar way as subscribe
.
This way, any mismatch is either impossible (i.e. compiler-enforced) or caught immediately in your API instead of causing unexpected casting failures later on in user code.
Edit: As discussed in the comments, if pinning the user on compile-time event values is ok, you can even template on the event num itself: https://godbolt.org/g/9NYVh3