# HG changeset patch # User David Demelier # Date 1472554090 -7200 # Node ID 5717cb946cd318b5bac7ab27078444a378576c73 # Parent 6d1579861b4a22cc1fb943544aefffd6d2c3a989 Signals: add new version of observer mechanism diff -r 6d1579861b4a -r 5717cb946cd3 CMakeLists.txt --- 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) diff -r 6d1579861b4a -r 5717cb946cd3 modules/signals/CMakeLists.txt --- /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 +# +# 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 +) diff -r 6d1579861b4a -r 5717cb946cd3 modules/signals/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 + * + * 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 +#include +#include +#include +#include +#include + +/** + * \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 +class Signal { +public: + /** + * \brief Opaque connection identifier. + * \see disconnect + */ + using Index = std::intmax_t; + +private: + using Function = std::function; + using List = std::vector; + using Stack = std::stack; + + List m_functions; + Stack m_reuseable; + +#if !defined(NDEBUG) + mutable bool m_locked{false}; +#endif + + template + 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(function); + m_reuseable.pop(); + } else { + m_functions.push_back(std::forward(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(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 diff -r 6d1579861b4a -r 5717cb946cd3 modules/signals/test/main.cpp --- /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 + * + * 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 + +#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(); +}