综合编程

C++ Signals

微信扫一扫,分享到朋友圈

C++ Signals

2020-02-15

While working on one of my current hobby projects I needed to broadcast a state
change to more than just one observer. I was quick to add my rather trustedEventEmitter. But I did hesitate, because something about the EventEmitter
always bothered me.

The problem with EventEmitter is that, although EventEmitter is type safe, the
type safety can only be asserted during runtime. A bit like JavaScript, where
it comes from, assigning wrong typed handlers will throw an exception when the
caller is actually called.

enum
{
FOO_EVENT
};
EventEmitter emitter;
emitter.on(FOO_EVENT, [] (int c) {
// do something
});
long change_value = 42;
emitter.emit(FOO_EVENT, change_value); // exception

This was always annoying, since there are so many different types that are
effectively the same, but the compiler really things are different. For example,
the types size_t, int and long can be the same type, and yet the compiler thinks
they are not.

The API of EventEmitter becomes event more frustrating, since modern compilers
allows for auto in almost all contexts. But since the context of the on function
is a template that cleverly derives it’s arguments from the function arguments.
You must either explicitly state your types in the function or as template
arguments to the on function.

EventEmitter emitter;
emitter.on(FOO_EVENT, [] (int c) {
// do something
});
emitter.on<int>(FOO_EVENT, [] (auto c) {
// do something
});
// this does not compile
emitter.on(FOO_EVENT, [] (auto c) {
// do something
});

So I looked at signals again. Well there is the venerable sigc++
, boost::signal
and lsignal
to name a few that I know about and looked at.
What all these libraries have in common is, they are HUGE for the simple task
of event multiplexing. Now, to some extent this is because they are carrying the
baggage of needing to support C++98 and that where quite dark times. But to some
extent allot of code is dedicated to solve the rather tricky issue of observer
dereregistration.

The problem boils down to the following, if the signal outlives it’s observer
and is triggered, it will crash. To this end a number of mechanics where
invented to implement automatic deregistration.

For example the sigc::trackable
implements a base class that will notify any
signal it was registered to and break the connection. This obviously requires
a large number of lists of back pointers and some clever registration logic.

After some consideration I looked at most of my usage patterns and they fall
into two categories, either the signal emitter and the observer are destroyed
virtually at the same time or the deregistration needs to be synced on something
that is not the object’s destruction.

And like always, when it’s my hobby project and there are not good alternatives,
I write my own: rsig

The basic usage is like all other signal libraries:

rsig::signal<unsigned int, unsigned int> processing_signal;
processing_signal.connect([&] (auto done, auto total) {
auto percent = static_cast<float>(done) / static_cast<float>(total) * 100.0f;
std::cout << "Handled " << done << " of " << total << " [" << percent << "%]" << std::endl;
});
for (auto i = 0u; i < items.size(); i++)
{
process_item(items[i]);
processing_signal.emit(i+1, items.size());
}

If the signal and the handler have similar lifetimes you can register the
observer on the signal and that is it. Eny time the signal is emitted it
will call the handler.

But life is not so simple, take the following a bit less contrived example.
You have a mouse class, a bit like so:

class Mouse
{
public:
rsig::signal<int, int>& get_move_signal()
{
return move_signal;
}
void update()
{
// get x and y from the OS
move_signal.emit(x, y);
}
private:
rsig::signal<int, int> move_signal;
};

If you want to observe the mouse motion in a game, you would write a player
controller a bit like so:

class PlayerController
{
public:
void activate(Mouse& mouse)
{
move_con = mouse.get_move_signal().connect([this] (auto x, auto y) {
control(x, y);
});
}
void deactivate(Mouse& mouse)
{
mouse.get_move_signal().disconnect(move_con);
}
void control(int x, int y)
{
// magic and unicorns
}
private:
rsig::connection move_con;
};

The connection object is a opaque handle to the handler registration. All you
need to do is save that handle and pass it to disconnect once you are done with
handling events.

The implementation is less than 100 lines of code:

struct connection
{
size_t id = 0;
void* signal = nullptr;
};
template <typename... Args>
class signal
{
public:
signal() = default;
~signal() = default;
connection connect(const std::function<void(Args...)>& fun);
void disconnect(connection id);
size_t emit(Args... args) const;
private:
mutable
std::mutex mutex;
size_t last_id = 0;
std::map<size_t, std::function<void (Args...)>> observers;
signal(const signal<Args...>&) = delete;
signal<Args...>& operator = (const signal<Args...>&) = delete;
};
template <typename... Args>
connection signal<Args...>::connect(const std::function<void(Args...)>& fun)
{
std::scoped_lock<std::mutex> sl(mutex);
if (!fun)
{
throw std::invalid_argument("Signal observer is invalid.");
}
auto id = ++last_id;
observers[id] = fun;
return {id, this};
}
template <typename... Args>
void signal<Args...>::disconnect(connection id)
{
if (id.signal != this)
{
throw std::invalid_argument("signal::disconnect: mismatched connection");
}
std::scoped_lock<std::mutex> sl(mutex);
auto i = observers.find(id.id);
if (i == end(observers))
{
throw std::runtime_error("No observer with this id.");
}
observers.erase(i);
}
template <typename... Args>
size_t signal<Args...>::emit(Args... args) const
{
std::scoped_lock<std::mutex> sl(mutex);
for (auto& [id, fun] : observers)
{
assert(fun);
fun(args...);
}
return observers.size();
}

If you remove legacy support for the dark ages of C++ and remove automatic
deregistration, the code is quite simple. You could even shave off a few
lines more if you like to live dangerous and think sanity checks are for the
weak.

C++ the good parts — std::swap

上一篇

Windows下MongoDB的配置及其远程连接

下一篇

你也可能喜欢

评论已经被关闭。

插入图片
Rioki's Corner 投稿者
我还没有学会写个人说明!
最近文章
  • 1 Immutable Containers and Thread Safety
  • 热门栏目

    C++ Signals

    长按储存图像,分享给朋友