changeset 589:5717cb946cd3

Signals: add new version of observer mechanism
author David Demelier <markand@malikania.fr>
date Tue, 30 Aug 2016 12:48:10 +0200
parents 6d1579861b4a
children 3058eaa603e6
files CMakeLists.txt modules/signals/CMakeLists.txt modules/signals/signals.hpp modules/signals/test/main.cpp
diffstat 4 files changed, 409 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Aug 29 21:16:40 2016 +0200
+++ b/CMakeLists.txt	Tue Aug 30 12:48:10 2016 +0200
@@ -58,6 +58,7 @@
 add_subdirectory(modules/js)
 add_subdirectory(modules/net)
 add_subdirectory(modules/options)
+add_subdirectory(modules/signals)
 add_subdirectory(modules/timer)
 add_subdirectory(modules/unicode)
 add_subdirectory(modules/xdg)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/signals/CMakeLists.txt	Tue Aug 30 12:48:10 2016 +0200
@@ -0,0 +1,22 @@
+#
+# CMakeLists.txt -- code building for common code
+#
+# Copyright (c) 2016 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+code_define_module(
+    NAME signals
+    SOURCES signals.hpp
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/signals/signals.hpp	Tue Aug 30 12:48:10 2016 +0200
@@ -0,0 +1,186 @@
+/*
+ * signals.h -- synchronous observer mechanism
+ *
+ * Copyright (c) 2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SIGNALS_HPP
+#define SIGNALS_HPP
+
+/**
+ * \file signals.hpp
+ * \brief Synchronous callbacks.
+ */
+
+#include <algorithm>
+#include <cstdint>
+#include <functional>
+#include <stack>
+#include <unordered_map>
+#include <vector>
+
+/**
+ * \brief Stores and call registered functions.
+ *
+ * This class is intended to be use as a public field in the desired object.
+ *
+ * The user just have to call one of connect(), disconnect() or the call
+ * operator to use this class.
+ *
+ * It stores the callable as std::function so type-erasure is complete.
+ *
+ * The user is responsible of taking care that the object is still alive
+ * in case that the function takes a reference to the object.
+ *
+ * It is forbidden to connect, disconnect, clearing the signal from a callback,
+ * instead you should postpone this operation for example, in an event loop
+ * event.
+ *
+ * It is also forbidden to throw in user callbacks, to make such guarantee, the
+ * call operator is noexcept.
+ */
+template <typename... Args>
+class Signal {
+public:
+    /**
+     * \brief Opaque connection identifier.
+     * \see disconnect
+     */
+    using Index = std::intmax_t;
+
+private:
+    using Function = std::function<void (Args...)>;
+    using List = std::vector<Function>;
+    using Stack = std::stack<Index>;
+
+    List m_functions;
+    Stack m_reuseable;
+
+#if !defined(NDEBUG)
+    mutable bool m_locked{false};
+#endif
+
+    template <typename Func>
+    inline Index add(Func &&function)
+    {
+        assert(!m_locked);
+
+        std::size_t id;
+
+        if (!m_reuseable.empty()) {
+            id = m_reuseable.top();
+            m_functions[id] = std::forward<Func>(function);
+            m_reuseable.pop();
+        } else {
+            m_functions.push_back(std::forward<Func>(function));
+            id = m_functions.size() - 1;
+        }
+
+        return id;
+    }
+
+public:
+    /**
+     * Register a new function to the signal.
+     *
+     * \pre must not be called from an handler
+     * \param function the function to copy
+     * \return the index for removing
+     * \throw exceptions on bad allocation
+     */
+    inline Index connect(const Function &function)
+    {
+        return add(function);
+    }
+
+    /**
+     * Overloaded function for move semantics.
+     *
+     * \pre must not be called from an handler
+     * \param function the function to move
+     * \return the connection index
+     * \throw exceptions on bad allocation
+     */
+    inline Index connect(Function &&function)
+    {
+        return add(function);
+    }
+
+    /**
+     * Disconnect an handler.
+     *
+     * \pre must not be called from an handler
+     * \param id the id to disconnect (will be set to 0)
+     */
+    inline void disconnect(Index &id)
+    {
+        assert(!m_locked);
+
+        if (id < 0 || static_cast<std::size_t>(id) >= m_functions.size())
+            return;
+
+        m_functions[id] = nullptr;
+        m_reuseable.push(id);
+        id = -1;
+    }
+
+    /**
+     * Remove all registered functions.
+     *
+     * \pre must not be called from an handler
+     */
+    inline void clear() noexcept
+    {
+        assert(!m_locked);
+
+        m_functions.clear();
+
+        while (!m_reuseable.empty())
+            m_reuseable.pop();
+    }
+
+    /**
+     * Get the number of signals connected.
+     *
+     * \return the number
+     */
+    inline std::size_t size() const noexcept
+    {
+        return std::count_if(m_functions.begin(), m_functions.end(), [&] (auto fn) {
+            return fn != nullptr;
+        });
+    }
+
+    /**
+     * Call every functions.
+     *
+     * \pre must not be called from an handler
+     * \param args the arguments to pass to the signal
+     */
+    void operator()(Args... args) const noexcept
+    {
+        assert(!m_locked);
+
+        m_locked = true;
+
+        for (auto &f : m_functions)
+            if (f)
+                f(args...);
+
+        m_locked = false;
+    }
+};
+
+#endif // !SIGNALS_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/signals/test/main.cpp	Tue Aug 30 12:48:10 2016 +0200
@@ -0,0 +1,200 @@
+/*
+ * main.cpp -- main test file for signals
+ *
+ * Copyright (c) 2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <gtest/gtest.h>
+
+#include "signals.hpp"
+
+using namespace testing;
+
+TEST(Connect, none)
+{
+    Signal<> sig;
+
+    sig();
+
+    ASSERT_EQ(0U, sig.size());
+}
+
+TEST(Connect, simple)
+{
+    Signal<> sig;
+
+    bool called = false;
+
+    sig.connect([&] () {
+        called = true;
+    });
+    sig();
+
+    ASSERT_EQ(1U, sig.size());
+    ASSERT_TRUE(called);
+}
+
+TEST(Connect, more)
+{
+    Signal<> sig;
+
+    bool f1 = false, f2 = false;
+
+    sig.connect([&] () {
+        f1 = true;
+    });
+    sig.connect([&] () {
+        f2 = true;
+    });
+    sig();
+
+    ASSERT_EQ(2U, sig.size());
+    ASSERT_TRUE(f1);
+    ASSERT_TRUE(f2);
+}
+
+TEST(Disconnect, simple)
+{
+    Signal<> sig;
+
+    auto called = false;
+    auto index = sig.connect([&] () {
+        called = true;
+    });
+    sig.disconnect(index);
+    sig();
+
+    ASSERT_EQ(0U, sig.size());
+    ASSERT_FALSE(called);
+}
+
+TEST(Disconnect, more)
+{
+    Signal<> sig;
+
+    auto called = false;
+    auto index = sig.connect([&] () {
+        called = true;
+    });
+    sig.disconnect(index);
+    sig();
+
+    ASSERT_EQ(0U, sig.size());
+    ASSERT_FALSE(called);
+    ASSERT_EQ(-1, index);
+}
+
+TEST(Disconnect, middle)
+{
+    Signal<> sig;
+
+    bool f1 = false, f2 = false, f3 = false;
+
+    sig.connect([&] () {
+        f1 = true;
+    });
+    auto idx = sig.connect([&] () {
+        f2 = true;
+    });
+    sig.connect([&] () {
+        f3 = true;
+    });
+    sig.disconnect(idx);
+    sig();
+
+    ASSERT_EQ(2U, sig.size());
+    ASSERT_TRUE(f1);
+    ASSERT_FALSE(f2);
+    ASSERT_TRUE(f3);
+}
+
+TEST(Disconnect, two)
+{
+    Signal<> sig;
+
+    bool f1 = false, f2 = false, f3 = false;
+
+    sig.connect([&] () {
+        f1 = true;
+    });
+    auto idx1 = sig.connect([&] () {
+        f2 = true;
+    });
+    auto idx2 = sig.connect([&] () {
+        f3 = true;
+    });
+    sig.disconnect(idx1);
+    sig.disconnect(idx2);
+    sig();
+
+    ASSERT_EQ(1U, sig.size());
+    ASSERT_TRUE(f1);
+    ASSERT_FALSE(f2);
+    ASSERT_FALSE(f3);
+}
+
+TEST(Disconnect, reuse)
+{
+    Signal<> sig;
+
+    bool f1 = false, f2 = false, f3 = false;
+
+    sig.connect([&] () {
+        f1 = true;
+    });
+    auto idx = sig.connect([&] () {
+        f2 = true;
+    });
+    sig.connect([&] () {
+        f3 = true;
+    });
+    sig.disconnect(idx);
+    sig.connect([&] () {
+        f2 = true;
+    });
+    sig();
+
+    ASSERT_EQ(3U, sig.size());
+    ASSERT_TRUE(f1);
+    ASSERT_TRUE(f2);
+    ASSERT_TRUE(f3);
+}
+
+TEST(Clear, basics)
+{
+    Signal<> sig;
+
+    bool f1 = false, f2 = false;
+
+    sig.connect([&] () {
+        f1 = true;
+    });
+    sig.connect([&] () {
+        f2 = true;
+    });
+    sig.clear();
+    sig();
+
+    ASSERT_EQ(0U, sig.size());
+    ASSERT_FALSE(f1);
+    ASSERT_FALSE(f2);
+}
+
+int main(int argc, char **argv)
+{
+    InitGoogleTest(&argc, argv);
+
+    return RUN_ALL_TESTS();
+}