I am in the process of writing a template class that would implement a Listener interface for every component that would need it within my application. I want a component to be able to listen to changes to an individual field within a listened object. The fields of each object are represented by an enum, and I've got a simple Notifier/Publisher that works great as such:
template <typename EFieldsEnum>
class AbstractNotifier
{
public:
void addListener(EFieldsEnum listenedField, INotifierListener<EFieldsEnum>* listener)
{
if (_listeners.find(listenedField) == _listeners.cend())
{
std::unordered_set<INotifierListener<EFieldsEnum>*> listeners;
listeners.insert(listener);
_listeners.emplace(listenedField, listeners);
}
else
{
_listeners.find(listenedField)->second.insert(listener);
}
}
void notify(EFieldsEnum updatedField)
{
for (INotifierListener<EFieldsEnum>* listener : _listeners[updatedField])
{
listener->onUpdate(updatedField);
}
}
protected:
std::unordered_map<EFieldsEnum, std::unordered_set<INotifierListener<EFieldsEnum>*>> _listeners;
};
I am trying to change the method signature slightly, in order for each listening component to register by passing a void() method that would be called as a callback, rather than implement the onUpdate generic method followed by a switch case. Ideally, I would want my listening components (say, Component1) to register without having to define a specific Listener interface for each type of Notifier that I will have, as such :
notifier.addListener(FieldEnum::MyField1, this, &Component1::onMyField1Updated);
and then Component2 could also register as such :
notifier.addListener(FieldEnum::MyField1, this, &Component2::someOtherMethod);
without Component1 and Component2 needing to implement a common interface. Would this be possible? I am struggling to figure out what type is expected to generically represent the member method that is passed through :
class IListener {};
void (IListener::*ListenerCallback)();
template <typename EFieldsEnum>
class AbstractNotifier
{
public:
void addListener(EFieldsEnum listenedField, IListener* listener, ListenerCallback listenerCallback)
{
std::pair<IListener*, ListenerCallback> pairToAdd = std::make_pair<IListener*, ListenerCallback>(listener, ListenerCallback);
if (_listeners.find(listenedField) == _listeners.cend())
{
std::unordered_set<std::pair<IListener*, ListenerCallback>> listenersAndCallbacks;
listenersAndCallbacks.insert(pairToAdd);
_listeners.emplace(listenedField, listenersAndCallbacks);
}
else
{
_listeners.find(listenedField)->second.insert(pairToAdd);
}
}
void notify(EFieldsEnum updatedField)
{
for (const std::pair<IListener*, ListenerCallback>& listenerAndCallback : _listeners[updatedField])
{
IListener* listener = listenerAndCallback.first;
ListenerCallback listenerCallback = listenerAndCallback.second;
listener->*listenerCallback();
}
}
protected:
std::unordered_map<EFieldsEnum, std::unordered_set<std::pair<IListener*, ListenerCallback>>> _listeners;
};
Use std::function<void()>
Here's a somewhat simplified version of your AbstractNotifier
that will accept callbacks from any class (or no class at all):
template <typename EFieldsEnum>
class AbstractNotifier
{
public:
void addListener(EFieldsEnum listenedField, std::function<void()> listenerCallback)
{
listeners_[listenedField].push_back(listenerCallback);
}
// Helper method template to wrap member function pointers
template <typename Listener>
void addListener(EFieldsEnum listenedField, Listener* listener, void(Listener::*listenerCallback)())
{
addListener(listenedField, [=]() { (listener->*listenerCallback)(); });
}
void notify(EFieldsEnum updatedField)
{
for (auto& callback : listeners_[updatedField])
{
callback();
}
}
protected:
std::unordered_map<EFieldsEnum, std::vector<std::function<void()>>> listeners_;
};
Note that I gave up the "uniqueness" constraint that your current implementation has and used a std::vector
. This is because it's difficult to impossible to really compare std::function
s, and it's easy to have very different looking std::function
s that end up calling back to the same underlying code.
i.e. All three of the following end up calling the same member function, but hold totally different types and can't be easily compared:
// Different lambdas, so different types
std::function<void()> f1([&someObject]() { someFunc.foo(); });
std::function<void()> f2([&someObject]() { someFunc.foo(); });
std::function<void()> f3(std::bind(&SomeType::foo, someObject));