signal: simplify the implementation of the cookie and signal

This commit is contained in:
Danny Robson 2020-07-23 15:16:15 +10:00
parent cf141e45d2
commit b3050c88c2
2 changed files with 127 additions and 153 deletions

View File

@ -12,114 +12,126 @@
#include "debug/assert.hpp" #include "debug/assert.hpp"
#include <functional> #include <functional>
#include <list>
namespace cruft { namespace cruft {
namespace reduce { /// A signal object whose clients can listen for invocations.
/// Returns the short-circuited logical-and of the results of all ///
/// connected callbacks. /// The arguments supplied to the invocation of the parent are supplied
struct logical_and { /// to the callback of each child.
template <typename InputT, typename ...Args> ///
decltype(auto) /// Every client must store the cookie from `connect`. When this goes out
operator() (InputT first, InputT last, Args&&... args) /// of scope it will disconnect the callback from the signal object.
{ /// It is permissible to release a cookie while it is being invoked, but
while (first != last) /// no other cookie for the duration.
if (!(std::invoke (*first++, args...))) ///
return false; /// All cookies _should_ be destroyed or released before the signal is
return true; /// destroyed.
} ///
}; /// \tparam FunctionT The type of the callback
/// Returns the short-circuited logical-or of the results of all
/// connected callbacks.
struct logical_or {
template <typename InputT, typename ...Args>
decltype(auto)
operator() (InputT first, InputT last, Args&&... args)
{
while (first != last)
if (std::invoke (*first++, args...))
return true;
return false;
}
};
/// Unconditionally evaluates all connected callbacks. Returns nothing.
struct noop {
template <typename InputT, typename ...Args>
void operator() (InputT first, InputT last, Args&&... args)
{
while (first != last) {
std::invoke (*first++, args...);
}
}
};
}
template < template <
typename FunctionT, typename FunctionT
typename ReductionT = reduce::noop
> >
class signal { class signal {
public: public:
using reduction_type = ReductionT;
using function_type = FunctionT; using function_type = FunctionT;
typedef std::list<FunctionT> group;
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
/// A single node in a doubly linked list of callbacks to invoke when
/// a signal is invoked.
///
/// They perform no direct processing by themselves. Instead they are
/// entirely focused on managing lifetimes of, and pointers to, the
/// callbacks that will be invoked.
struct cookie { struct cookie {
cookie (cookie const&) = delete; cookie (FunctionT &&_callback)
cookie& operator= (cookie const&) = delete; : callback (std::move (_callback))
, next (nullptr)
cookie (typename group::iterator _position, , prev (nullptr)
signal<FunctionT,ReductionT> &_parent):
m_position (_position),
m_parent (_parent)
{ ; } { ; }
cookie (cookie &&rhs) noexcept
cookie (cookie &&rhs) noexcept: : cookie (std::move (rhs.callback))
m_position (rhs.m_position),
m_parent (rhs.m_parent)
{ {
rhs.m_position = rhs.m_parent.m_children.end (); replace (rhs);
} }
cookie& operator= (cookie &&rhs) noexcept cookie& operator= (cookie &&rhs) noexcept
{ {
CHECK_EQ (&m_parent, &rhs.m_parent); replace (rhs);
std::swap (m_position, rhs.m_position); callback = std::move (rhs.callback);
return *this; return *this;
} }
cookie (cookie const&) = delete;
cookie& operator= (cookie const&) = delete;
~cookie () ~cookie ()
{ {
if (m_parent.m_children.end () != m_position) release ();
m_parent.disconnect (*this);
} }
/// Take the position of `rhs` in the linked list.
void reset (FunctionT &&cb) void replace (cookie &rhs)
{ {
*m_position = std::move (cb); // It could be more efficient to special case these
// operations but this shouldn't be an operation that
// occurs in a hot loop anyway.
release ();
rhs.append (*this);
rhs.release ();
} }
/// Link the provided cookie into the linked list as our
/// successor.
///
/// The provided cookie must not currently be part of any
/// linked list.
void append (cookie &rhs)
{
CHECK (rhs.next == nullptr);
CHECK (rhs.prev == nullptr);
typename group::iterator m_position; if (next) {
signal<FunctionT,ReductionT> &m_parent; CHECK_EQ (next->prev, this);
next->prev = &rhs;
}
rhs.next = next;
rhs.prev = this;
next = &rhs;
}
/// If this cookie is part of a linked list then remove it
/// from the linked list. Otherwise this is a noop.
void release (void)
{
if (next) next->prev = prev;
if (prev) prev->next = next;
next = nullptr;
prev = nullptr;
}
FunctionT callback;
/// The next node in the linked list. Or nullptr if this is the end.
cookie *next;
/// The previous node in the linked list. Or nullptr if this is the start.
cookie *prev;
}; };
public: public:
signal () = default; signal ()
: m_head (FunctionT{})
{ ; }
signal (signal const&) = default; signal (signal const&) = delete;
signal& operator= (signal const&) = default; signal& operator= (signal const&) = delete;
signal (signal &&) noexcept = default;
signal& operator= (signal &&) noexcept = default;
signal (signal &&rhs) noexcept = default;
signal& operator= (signal &&rhs) noexcept = default;
~signal () ~signal ()
{ {
@ -127,65 +139,59 @@ namespace cruft {
} }
/// Add a callback to list. /// Add a callback to list.
cookie connect [[nodiscard]] (FunctionT &&_cb) cookie
connect [[nodiscard]] (FunctionT &&_callback)
{ {
return cookie ( cookie res (std::move (_callback));
m_children.insert ( m_head.append (res);
m_children.end (), return res;
std::move (_cb)
),
*this
);
} }
cookie connect [[nodiscard]] (FunctionT const &_cb)
{
return cookie (
m_children.insert (
m_children.end (),
std::move (_cb)
),
*this
);
}
void disconnect (cookie &c)
{
m_children.erase (c.m_position);
c.m_position = m_children.end ();
}
/// Disconnect all callbacks /// Disconnect all callbacks
void clear (void) void clear (void);
{
m_children.clear ();
}
/// Returns the number of callbacks connected. /// Returns the number of callbacks connected.
size_t size (void) const std::size_t size (void) const
{ {
return m_children.size (); std::size_t accum = 0;
for (auto cursor = m_head.next; cursor; cursor = cursor->next)
++accum;
return accum;
} }
bool empty (void) const bool empty (void) const
{ {
return m_children.empty (); return m_head.next == nullptr;
} }
/// Execute all callbacks /// Execute all callbacks
template <typename ...ArgsT> template <typename ...ArgsT>
decltype(auto) decltype(auto)
operator() (ArgsT&&... tail) operator() (ArgsT&&... tail)
{ {
return ReductionT {} ( // We need to cache the cursor and advance _before_ we invoke
m_children.begin (), // the cookie so that we have saved a pointer to the next
m_children.end (), // child in case it gets released on us during the call.
std::forward<ArgsT> (tail)... cookie *cursor = m_head.next;
);
while (cursor) {
auto now = cursor;
cursor = cursor->next;
std::invoke (now->callback, tail...);
}
} }
private: private:
group m_children; /// The first node in the linked list of callbacks. We
/// unconditionally store this node to simplify anchoring the
/// linked list to the signal object and removing nodes via their
/// destructors.
cookie m_head;
}; };

View File

@ -72,57 +72,26 @@ test_value_signal (cruft::TAP::logger &tap)
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
void void
test_combiner (cruft::TAP::logger &tap) test_parallel_release (cruft::TAP::logger &tap)
{
{
cruft::signal<std::function<bool(void)>, cruft::reduce::logical_and> sig;
unsigned count = 0;
auto cookie0 = sig.connect ([&] (void) { ++count; return true; });
auto cookie1 = sig.connect ([&] (void) { ++count; return true; });
auto cookie2 = sig.connect ([&] (void) { ++count; return true; });
tap.expect (sig (), "bool signal, success");
tap.expect_eq (count, 3u, "bool signal, success, count");
}
{
cruft::signal<std::function<bool(void)>, cruft::reduce::logical_and> sig;
unsigned count = 0;
auto cookie0 = sig.connect ([&] (void) { ++count; return true; });
auto cookie1 = sig.connect ([&] (void) { ++count; return false; });
auto cookie2 = sig.connect ([&] (void) { ++count; return true; });
tap.expect (!sig (), "bool signal, failure");
// ordering of signals is not guaranteed so we can't say for sure how
// many callbacks will be triggered; it will _probably_ be in order
// though.
tap.expect_le (count, 3u, "bool signal, failure, count");
}
}
///////////////////////////////////////////////////////////////////////////////
void
test_disconnect (cruft::TAP::logger &tap)
{ {
tap.expect_nothrow ([] { tap.expect_nothrow ([] {
using function_t = std::function<void(void)>; using function_t = std::function<void(void)>;
cruft::signal<function_t> sig; cruft::signal<function_t> sig;
cruft::signal<function_t>::cookie a = sig.connect ([&] (void) { sig.disconnect (a); }); cruft::signal<function_t>::cookie a = sig.connect ([&] (void) { a.release (); });
cruft::signal<function_t>::cookie b = sig.connect ([&] (void) { sig.disconnect (b); }); cruft::signal<function_t>::cookie b = sig.connect ([&] (void) { b.release (); });
cruft::signal<function_t>::cookie c = sig.connect ([&] (void) { sig.disconnect (c); }); cruft::signal<function_t>::cookie c = sig.connect ([&] (void) { c.release (); });
cruft::signal<function_t>::cookie d = sig.connect ([&] (void) { sig.disconnect (d); }); cruft::signal<function_t>::cookie d = sig.connect ([&] (void) { d.release (); });
sig (); sig ();
}, "parallel disconnect in invocation"); }, "parallel release in invocation");
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
#include <iostream>
int int
main (int, char **) main (int, char **)
{ {
@ -131,7 +100,6 @@ main (int, char **)
test_single (tap); test_single (tap);
test_double (tap); test_double (tap);
test_value_signal (tap); test_value_signal (tap);
test_combiner (tap); test_parallel_release (tap);
test_disconnect (tap);
}); });
} }