I have found that boost::signals2 uses sort of a lazy deletion of connected slots, which makes it difficult to use connections as something that manages lifetimes of objects. I
This is an incredibly annoying aspect of boost::signals2.
The approach I took to resolve it is to store the signal in a scoped_ptr, and when I want to force disconnection of all slots, I delete the signal. This only works in cases when you want to forcefully disconnect all connections to a signal.
Is the behaviour any more strict with a scoped_connection?
So, rather than:
void Execute() {
m_WorkerConnection = m_MyWorker.OnWorkDone.connect(boost::bind
(&Command::Handle_OnWorkComplete, shared_from_this());
// launch asynchronous work here and return
}
...
boost::signals2::connection m_WorkerConnection;
Instead using:
void Execute() {
boost::signals2::scoped_connection m_WorkerConnection
(m_MyWorker.OnWorkDone.connect(boost::bind
(&Command::Handle_OnWorkComplete, shared_from_this()));
// launch asynchronous work here and return
} // connection falls out of scope
(copy-constructed from a boost::signals2::connection
)
I've not used any sort of signalling so it's more of a guess than anything else, but following Execute()
you wouldn't need to disconnect()
, since scoped_connection handles it for you. That's more of a 'simplify the design' rather than actually solving your problem. But it may mean that you can Execute()
and then immediately ~Command()
(or delete
the shared_ptr).
Hope that helps.
EDIT: And by Execute()
then immediately ~Command()
I obviously mean from outside your Command object. When you construct the Command to execute it, you should then be able to say:
cmd->Execute();
delete cmd;
Or similar.
I stumbled upon the same problem and i really miss some kind of explicit cleanup in the API.
In my scenario i am unloading some plug-in dll's and i have to assure there are no dangling objects (slots) which refer to code (vftables or whatsoever) living in the unloaded dll. Simply disconnecting slots didn't work due to the lazy deletion stuff.
My first workaround was a signal wrapper which tweaks the disconnecting code a little bit:
template <typename Signature>
struct MySignal
{
// ...
template <typename Slot>
void disconnect (Slot&& s)
{
mPrivate.disconnect (forward (s));
// connect/disconnect dummy slot to force cleanup of s
mPrivate.connect (&MySignal::foo);
mPrivate.disconnect (&MySignal::foo);
}
private:
// dummy slot function with matching signature
// ... foo (...)
private:
::boost::signals2::signal<Signature> mPrivate;
};
Unfortunately this didn't work because connect()
only does some cleanup. It doesn't guarantee cleanup of all unconnected slots. Signal invocation on the other hand does a full cleanup but a dummy invocation would also be an unacceptable behavioral change (as already mentioned by others).
In the absence of alternatives i ended up in patching the original signal
class (Edit: i really would appreciate a built-in solution. this patch was my last resort). My patch is around 10 lines of code and adds a public cleanup_connections()
method to signal
. My signal wrapper invokes the cleanup at the end of the disconnecting methods. This approach solved my problems and i didn't encounter any performance problems so far.
Edit: Here is my patch for boost 1.5.3
Index: signals2/detail/signal_template.hpp
===================================================================
--- signals2/detail/signal_template.hpp
+++ signals2/detail/signal_template.hpp
@@ -220,6 +220,15 @@
typedef mpl::bool_<(is_convertible<T, group_type>::value)> is_group;
do_disconnect(slot, is_group());
}
+ void cleanup_connections () const
+ {
+ unique_lock<mutex_type> list_lock(_mutex);
+ if(_shared_state.unique() == false)
+ {
+ _shared_state.reset(new invocation_state(*_shared_state, _shared_state->connection_bodies()));
+ }
+ nolock_cleanup_connections_from(false, _shared_state->connection_bodies().begin());
+ }
// emit signal
result_type operator ()(BOOST_SIGNALS2_SIGNATURE_FULL_ARGS(BOOST_SIGNALS2_NUM_ARGS))
{
@@ -690,6 +699,10 @@
{
(*_pimpl).disconnect(slot);
}
+ void cleanup_connections ()
+ {
+ (*_pimpl).cleanup_connections();
+ }
result_type operator ()(BOOST_SIGNALS2_SIGNATURE_FULL_ARGS(BOOST_SIGNALS2_NUM_ARGS))
{
return (*_pimpl)(BOOST_SIGNALS2_SIGNATURE_ARG_NAMES(BOOST_SIGNALS2_NUM_ARGS));
boost::signals2
does clean up the slots during connect/invoke.
So if all the slots disconnect themselves from the signal, invoking the signal a second time will not call anything but it should clean up the slots.
To answer your comment, yes, invoking the signal again is not safe if there are be other slots connected, as they will be invoked again. In that case I suggest you go the other way around and connect a dummy slot, then disconnect it when your "real" slot is invoked. Connecting another slot will clean up stale connections, so your slot should be released.
Just make sure that you don't keep any references that need freeing in the dummy slot, or you're back where you started.
I ended up doing my own (subset) implementation of a signal, the main requirement being that a slot should be destroyed by a call to connection::disconnect().
The implementation goes along the lines of the signal storing all slots in a map from slot implementation pointer to a shared_ptr for a slot implementation instead of a list/vector, thereby giving quick access to individual slots without having to iterate over all slots. A slot implementation is in my case basically a boost::function.
Connections have a weak_ptr to the internal implementation class for the signal and a weak_ptr to the slot implementation type to allow the signal to go out of scope and to use the slot pointer as the key into the signal map as well as an indication on whether the connection is still active (can't use a raw pointer as that could potentially be reused).
When disconnect is called, both of these weak pointers are converted to shared_ptrs and if both of these succeed, the signal implementation is asked to disconnect the slot given by the pointer. This is done by simple erasing it from the map.
The map is protected by a mutex to allow for multithreaded use. To prevent deadlocks, the mutex is not held while calling the slots, however this means that a slot may be disconnected from a different thread just prior to being called by the signal. This is also the case with regular boost::signals2 and in both of these scenarios one needs to be able to handle a callback from a signal even after one has disconnected.
To simplify the code for when the signal is fired, I am forcing all slots to be disconnected during this. This is different from boost::signals2, that does a copy of the list of slots before calling them in order to handle disconnections/connections while firing the signal.
The above works well for my scenario, where the signal of interest is fired very seldom (and in that case only once) but there are a lot of short-lived connections that otherwise use up a lot of memory even when using the trick outlined in the question.
For other scenarios, I've been able to replace the use of a signal with just a boost::function (thus requiring that there can only be a single connection) or just by sticking with the workaround in the question where the listener itself manages its lifetime.