As can be seen in the code below (implemented as an illustration of the problem), I'm trying to send a signal from an inner class to a mid class, which will relay it to an outer class.
#include <boost/bind.hpp>
#include <boost/signals2.hpp>
#include <iostream>
class inner {
public:
template <class T>
void register_callback(boost::function<void(T *)> cb, T *obj)
{
sig_inner_.connect(boost::bind(cb, boost::ref(obj)));
}
void trigger()
{
std::cout << "inner" << std::endl;
sig_inner_();
}
private:
boost::signals2::signal<void()> sig_inner_;
};
class mid {
public:
mid() { inner_obj.register_callback<mid>(&mid::handle_sig_mid, this); }
template <class T>
void register_callback(boost::function<void(T *)> cb, T *obj)
{
sig_mid_.connect(boost::bind(cb, boost::ref(obj)));
}
void trigger() { sig_mid_(); }
void inner_trigger() { inner_obj.trigger(); }
void handle_sig_mid()
{
std::cout << "mid" << std::endl;
trigger();
}
private:
boost::signals2::signal<void()> sig_mid_;
inner inner_obj;
};
class outer {
public:
outer() { mid_obj.register_callback<outer>(&outer::handle_sig_outer, this); }
void inner_trigger() { mid_obj.inner_trigger(); }
private:
mid mid_obj;
void handle_sig_outer() { std::cout << "outer" << std::endl; }
};
int main()
{
outer outer_obj;
outer_obj.inner_trigger();
return 0;
}
Instead of the desired result of:
inner
mid
outer
When running the program, what actually happens is:
inner
mid
mid
Followed by a crash.
I already noticed that the address of 'this' is different in the handler than what I would expect in a regular method, but I don't know how work around that.
The only solution I found for this was connecting a signal in the outer class to its handler, and then storing the pointer(unique_ptr, in this case) in the inner class, thus avoiding the need to relay it, but it doesn't feel like a safe way to use signals.
I'm kind of new to c++, and to boost in particular, so I don't really know how trigger callbacks in the outer class from the inner class in a clean and safe way.
Two things:
When you bind to the boost::ref(obj)
you make the bind-expression hold a reference to the function parameter, which goes out of scope at the exit from register_callback
. (See Does boost::bind() copy parameters by reference or by value?)
Just bind to the pointer itself, which makes the bind-expression hold a copy of the pointer itself.
It's important to be aware of lifetime issues when registering callbacks. In general, you must unregister before destroying any bound object in the signal slot.
In your example this doesn't really occur, because the connected slots all exist in member objects. This means that the slots get destructed before the outer object(s) disappear.
However, if something gets copied/moved that breaks down. The usual pattern to combat this is to use scoped_connection
s.
Let me show my suggestions in two steps:
There's no need to template register_callback
because you type-erase the object type T
immediately using bind
and the nullary signal slot.
So, instead make it take an arbitrary nullary and do the bind in the caller? In fact, prefer to use a lambda at the caller.
template <class F> void register_callback(F&& f) {
sig_inner_.connect(std::forward<F>(f));
}
And then
mid() { inner_obj.register_callback([this] { handle_sig_mid(); }); }
Instead of using the heavy-weight option and using enable_shared_from_this()
with dynamic allocation everywhere, use the library facilities: http://www.boost.org/doc/libs/1_65_1/doc/html/boost/signals2/scoped_connection.html
Note In your example, using
shared_from_this()
is out of the question because it's not valid inside the constructor.
My suggestion:
template <class F> boost::signals2::scoped_connection register_callback(F&& f) {
return sig_inner_.connect(std::forward<F>(f));
}
And then
mid() { _connection = inner_obj.register_callback([this] { handle_sig_mid(); }); }
Make _connection
a member:
boost::signals2::scoped_connection _connection;
This way, the slots get disconnected when the containing class is destructed.
#include <boost/bind.hpp>
#include <boost/signals2.hpp>
#include <iostream>
class inner {
public:
template <class F> boost::signals2::scoped_connection register_callback(F&& f) {
return sig_inner_.connect(std::forward<F>(f));
}
void trigger() {
std::cout << "inner" << std::endl;
sig_inner_();
}
private:
boost::signals2::signal<void()> sig_inner_;
};
class mid {
public:
mid() { _connection = inner_obj.register_callback([this] { handle_sig_mid(); }); }
template <class F> boost::signals2::scoped_connection register_callback(F&& f) {
return sig_mid_.connect(std::forward<F>(f));
}
void trigger() { sig_mid_(); }
void inner_trigger() { inner_obj.trigger(); }
void handle_sig_mid() {
std::cout << "mid" << std::endl;
trigger();
}
private:
boost::signals2::scoped_connection _connection;
boost::signals2::signal<void()> sig_mid_;
inner inner_obj;
};
class outer {
public:
outer() { _connection = mid_obj.register_callback([this] { handle_sig_outer(); }); }
void inner_trigger() { mid_obj.inner_trigger(); }
private:
boost::signals2::scoped_connection _connection;
mid mid_obj;
void handle_sig_outer() { std::cout << "outer" << std::endl; }
};
int main() {
outer outer_obj;
outer_obj.inner_trigger();
return 0;
}
Prints
inner
mid
outer