note: A version of this code without link lifetime management was previously reviewed. Adding lifetime management actually made the code simpler, and the class simpler to use.
I have written a class that handles listeners registering callbacks to receive messages. Link lifetime is managed. Code is multithreaded in that any thread can broadcast to the listeners, and any thread can add or remove listeners.
I have made a list of requirements for the class:
- there may be zero, one or multiple listeners
- listeners can be registered or deregistered from different threads
- each listener should be called exactly once for each message
- all listeners should receive all messages
- messages may originate from multiple sources in different threads
- each message should be forwarded to all listeners before the next message is processed (messages are not sent in parallel)
- each listener should receive the messages in the same order, i.e., the order in which they are provided to the broadcaster (from the view of the broadcaster)
- listeners can be removed
- listeners are identified by a cookie whose validity state indicates if the listener is still listening
- anyone with the cookie should be able to deregister the listener
- listeners should never be called again when deregistration completes
- listeners should be able to add a new listener when invoked, this listener will participate starting from the next message
- listeners are noexcept, so code shouldn’t deal with exceptions thrown by listeners
- adding and removing listeners happens much less frequently than broadcasting.
A further consideration that is not enforced in code:
- Users are expected to read documentation that states which functions are safe to call from inside a listener, and which must not.
Link lifetime management is performed by wrapper each registered listener function in a shared_ptr that is held by the listener, and references as a weak pointer in the broadcaster class. Just before the listener is invoked, the weak pointer is converted to a shared pointer, which would fail if the listener released its end.
I am not sure if this method for lifetime management works, or if there is a race condition: broadcaster obtains shared pointer, but before it can invoke the callback, the listener just destructs. Link remains valid as shared pointer is acquired by broadcaster, but the listener that is about to be invoked no longer exists. That is a possible race, right? How to solve it?
A listener who owns a dangling link (no more broadcaster) is not a problem, as they can't do anything unsafe with their end of the link.
Implementation
#include <vector>
#include <iterator>
#include <mutex>
#include <memory>
#include <functional>
#include <type_traits>
// based on https://stackoverflow.com/a/47872677
// usage notes:
// 1. None of the member functions of this class may be called from
// inside an executing listener, except
// add_listener_from_inside_callback(). Calling them anyway
// will result in deadlock.
// 2. add_listener_from_inside_callback() may _only_ be called from
// inside an executing listener.
template <class... Message>
class Broadcaster
{
public:
using listener = std::function<void(Message...)>;
using cookieType = std::shared_ptr<void>;
// returns number of registered listeners
size_t num_listeners() noexcept
{
auto l = std::unique_lock(_mut);
// prune to reduce chance that we return
// having more listeners than we do
for (auto it = _targets.cbegin(); it != _targets.cend(); )
{
if (it->expired())
it = _targets.erase(it);
else
++it;
}
return _targets.size();
}
// clears all listeners
void clear() noexcept
{
auto l = std::unique_lock(_mut);
_targets.clear();
}
// should never be called from inside a running listener
template <class F> requires (std::is_nothrow_invocable_r_v<void, F, Message...>)
[[nodiscard]] cookieType add_listener(F&& r_)
{
auto l = std::unique_lock(_mut);
auto listenFunc = std::make_shared<listener>(std::forward<F>(r_));
_targets.push_back(listenFunc);
return listenFunc;
}
template <class F> requires (std::is_nothrow_invocable_r_v<void, F, Message...>)
[[nodiscard]] cookieType add_listener_from_inside_callback(F&& r_)
{
auto listenFunc = std::make_shared<listener>(std::forward<F>(r_));
// place into a holding pen, will be added to active listeners
// as soon as all currently registered callbacks have received
// the current message
_newTargets.push_back(listenFunc);
return listenFunc;
}
void notify_all(const Message&... msg_) noexcept
{
auto l = std::unique_lock(_mut);
// invoke all listeners with message, if still
// alive. else remove
for (auto it = _targets.cbegin(); it != _targets.cend(); )
{
auto targetPtr = it->lock();
if (!targetPtr)
it = _targets.erase(it);
else
{
targetPtr->operator()(msg_...);
++it;
}
}
// if any new listeners, move them into active listeners
// so they'll receive the next message
if (!_newTargets.empty())
{
_targets.insert(_targets.end(), std::make_move_iterator(_newTargets.begin()),
std::make_move_iterator(_newTargets.end()));
_newTargets.clear();
}
}
private:
mutable std::mutex _mut;
std::vector<std::weak_ptr<listener>> _targets;
std::vector<std::weak_ptr<listener>> _newTargets;
};
Example usage
#include <iostream>
#include <string>
#include <functional>
void freeTestFunction(std::string msg_) noexcept
{
std::cout << "from freeTestFunction: " << msg_ << std::endl;
}
struct test
{
using StringBroadcaster = Broadcaster<std::string>;
void simpleCallback(std::string msg_) noexcept
{
std::cout << "from simpleCallback: " << msg_ << std::endl;
}
void oneShotCallback(std::string msg_) noexcept
{
std::cout << "from oneShotCallback: " << msg_ << std::endl;
_cb_oneShot_cookie.reset();
}
void twoStepCallback_step1(std::string msg_) noexcept
{
std::cout << "from twoStepCallback_step1: " << msg_ << std::endl;
// replace callback
_cb_twostep_cookie = _broadcast.add_listener_from_inside_callback([&](auto fr_) noexcept { twoStepCallback_step2(fr_); });
}
void twoStepCallback_step2(std::string msg_) noexcept
{
std::cout << "from twoStepCallback_step2: " << msg_ << std::endl;
}
void runExample()
{
auto cb_simple_cookie = _broadcast.add_listener([&](auto fr_) noexcept { simpleCallback(fr_); });
_cb_oneShot_cookie = _broadcast.add_listener([&](auto fr_) noexcept { oneShotCallback(fr_); });
_cb_twostep_cookie = _broadcast.add_listener([&](auto fr_) noexcept { twoStepCallback_step1(fr_); });
auto free_func_cookie = _broadcast.add_listener(&freeTestFunction);
_broadcast.notify_all("message 1"); // should be received by simpleCallback, oneShotCallback, twoStepCallback_step1, freeTestFunction
free_func_cookie.reset();
_broadcast.notify_all("message 2"); // should be received by simpleCallback and twoStepCallback_step2
cb_simple_cookie.reset();
_broadcast.notify_all("message 3"); // should be received by twoStepCallback_step2
_cb_twostep_cookie.reset();
_broadcast.notify_all("message 4"); // should be received by none
}
StringBroadcaster _broadcast;
StringBroadcaster::cookieType _cb_oneShot_cookie;
StringBroadcaster::cookieType _cb_twostep_cookie;
};
int main(int argc, char **argv)
{
test t;
t.runExample();
return 0;
}