Implemented observers in C++11 in a generic and statically accessible way. Though an instance is created, it's used only for cleanup and lifetime management.
template<typename... Args>
class Observers
{
private:
unordered_map<int, vector<function<void(Args...)>>> observers;
static Observers<Args...> *instance;
public:
Observers()
{
instance = this;
}
template<typename Observer>
static void Register(int ev, Observer &&observer)
{
instance->observers[ev].push_back(forward<Observer>(observer));
}
static void Notify(int ev, Args... args)
{
try
{
auto &obs = instance->observers.at(ev);
for (auto &o : obs)
o(args...);
}
catch (...){}
}
};
template<typename... Args>
Observers<Args...> *Observers<Args...>::instance;
usage:
using IntIntObservers = Observers < int, int > ;//optional type declaration
IntIntObservers o;//observer instance
IntIntObservers::Register(1, [=](int a, int b){whatever});
IntIntObservers::Notify(1, 4, 5);
An observer set can be accessed statically as long as the instance is alive. Multiple instances of the same type could be achieved by adding an id parameter to the template declaration, making it
template<int id, typename... Args>
Anything that I missed and could be improved?
1 Answer 1
Briefly, I find the following:
The current static behaviour is not intuitive (what does
int ev
really mean? why numbers and not names?), not efficient (why search in anunordered map
rather that direct access?) and not thread-safe (instance
is shared among all threads).A
list
might be more appropriate to store observers of one event, enabling easier management like removing an observer.Catching all exceptions in such a generic tool is a limitation. If needed, it could be at least parametrized.
Parameter pack
Args...
does not take into account return types.Your
Notify
takes its arguments by value. Forwarding is not appropriate when calling multiple functions, but at least arguments should be passed by reference.
At a very basic level, I would prefer an object, called e.g. signal
, templated on full function signature e.g. R(A...)
, with non-static members:
(private)
observers
:std::list
or some associative container ofstd::function<R(A...)>
add()/remove()
orconnect()/disconnect()
to manage the content ofobservers
.remove()
would need some form of identification of existing observers, hence the need for an associative container.operator()(A&&... a)
so thatsignal
behaves like a function object. Argumentsa...
are passed without forwarding to underlying functions, so that they are copied if needed, and not invalidated between subsequent calls.additional management like
clear()/empty()
.
Your example usage would be rather like
signal<void(int, int)> s;
s.connect([=](int a, int b){whatever});
s(4, 5);
Isn't that better? Since everything is non-static, the user controls how many instances there are, and names them properly. In fact, a single object may have e.g. ten methods, each with its own observers. Each such method is then a signal and behaves like a function object.
Additionally
std::function
expects the 1st argument of a call to be the object if bound on a member function. It would be much more convenient to have functionality such that the object is stored separately and thesignal
's signature matches exactly that of the member function. In many applications everything is about objects + member functions rather than free functions.Though non-trivial, possibly provide a mechanism to collect return values of called (observer) functions plus policies to choose, compute and return a single return value from
operator()
.
Have a look at boost::signals and boost::signals2 for more ideas.
I have implemented my own model in the past, using delegates instead of std::function
. There is an amazing amount of additional work if such "signals" are to fully mimic functions or member functions, including:
- default arguments (arbitrary values encoded into types in the signature itself)
- overloading (with different signatures)
- inheritance (overriding member functions in base/derived classes)
- virtual functions
- ...
I have used it as a full C++-compliant replacement for Qt signals/slots and wrapped most of the Qt Gui module so that a project can be built without moc and without qmake. One day I will hopefully clean and rewrite in C++11.
-
\$\begingroup\$ In my experience, std::list is slower than std::vector, even when complexity says otherwise. Arrays are just that much faster on C++. The event type is an int because I felt like using enums, it could be a templated type. \$\endgroup\$DariusL– DariusL2014年05月07日 10:00:19 +00:00Commented May 7, 2014 at 10:00
-
\$\begingroup\$ @user1560102
std::list
: depends on usage. Event "types": ok, let us put all ourdouble
variables of a program into a staticunordered_map<int, double>
calledDoubleVariables
and use a number instead of a name toRegister
orGet
each variable as an entry in this map. How does this look? To me, it looks like assembly. \$\endgroup\$iavr– iavr2014年05月07日 10:54:26 +00:00Commented May 7, 2014 at 10:54