From 7504646301a91c38462016082196a1e408771b58 Mon Sep 17 00:00:00 2001 From: John Wellbelove Date: Sun, 3 Aug 2025 16:22:50 +0100 Subject: [PATCH] Added delegate_observable --- include/etl/delegate_observable.h | 208 +++++++++++++++++++++++ include/etl/delegate_observer.h | 262 ----------------------------- test/CMakeLists.txt | 1 + test/test_delegate_observable.cpp | 264 ++++++++++++++++++++++++++++++ test/vs2022/etl.vcxproj | 2 + test/vs2022/etl.vcxproj.filters | 13 ++ 6 files changed, 488 insertions(+), 262 deletions(-) create mode 100644 include/etl/delegate_observable.h delete mode 100644 include/etl/delegate_observer.h create mode 100644 test/test_delegate_observable.cpp diff --git a/include/etl/delegate_observable.h b/include/etl/delegate_observable.h new file mode 100644 index 00000000..d7ce877d --- /dev/null +++ b/include/etl/delegate_observable.h @@ -0,0 +1,208 @@ +///\file + +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2023 John Wellbelove + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ + +#ifndef ETL_DELEGATE_OBSERVABLE_INCLUDED +#define ETL_DELEGATE_OBSERVABLE_INCLUDED + +#include "platform.h" +#include "delegate.h" +#include "array.h" + +namespace etl +{ + //********************************************************************* + /// An obserable type that uses delegates to notify observers. + ///\tparam TNotification The notification type sent to the observers. + ///\tparam Max_Observers The maximum number of observers that can be accommodated. + ///\ingroup observer + //********************************************************************* + template + class delegate_observable + { + public: + + /// The type of the observers. + typedef etl::delegate delegate_type; + + private: + + typedef etl::array DelegateList; + + public: + + /// The type for sizes. + typedef size_t size_type; + + /// The type of the notification. + typedef TNotification notification_type; + + //***************************************************************** + /// Default constructor. + //***************************************************************** + ETL_CONSTEXPR14 delegate_observable() + : delegate_list() + , delegate_count(0) + { + } + +#if ETL_USING_CPP11 + //***************************************************************** + /// Construct from a list of observers. + //***************************************************************** + template + ETL_CONSTEXPR14 delegate_observable(TDelegate&&... delegates) + : delegate_list{ etl::forward(delegates)... } + , delegate_count(sizeof...(delegates)) + { + ETL_STATIC_ASSERT(Max_Observers >= sizeof...(delegates), "Number of delegates exceeds maximum observers"); + ETL_STATIC_ASSERT((etl::are_all_same...>::value), "All delegates must be delegate_type"); + } + + //***************************************************************** + /// Construct from notification type and a list of observers. + /// Variant for template deduction guide. + /// The notification value is ignored. It is just here to allow deduction + /// of the notification type for the template deduction guide. + //***************************************************************** + template + ETL_CONSTEXPR14 delegate_observable(notification_type, TDelegate&&... delegates) + : delegate_observable(etl::forward(delegates)...) + { + } +#endif + + //***************************************************************** + /// Add an observer to the list. + ///\param observer A reference to the observer. + ///\return true if the observer was added or already exists, false if not. + //***************************************************************** + ETL_CONSTEXPR14 bool add_observer(delegate_type observer) + { + for (size_t i = 0; i < Max_Observers; ++i) + { + if (delegate_list[i] == observer) + { + // Already there, so just return. + return true; + } + } + + // If we get here, then we need to add it. + for (size_t i = 0; i < Max_Observers; ++i) + { + if (!delegate_list[i].is_valid()) + { + // Found an empty slot, so add it. + delegate_list[i] = observer; + ++delegate_count; + return true; + } + } + + return false; + } + + //***************************************************************** + /// Remove a particular observer from the list. + ///\param observer A reference to the observer. + ///\return true if the observer was removed, false if not. + //***************************************************************** + ETL_CONSTEXPR14 bool remove_observer(const delegate_type& observer) + { + for (size_t i = 0; i < Max_Observers; ++i) + { + if (delegate_list[i] == observer) + { + // Clear it. + delegate_list[i].clear(); + --delegate_count; + return true; + } + } + + return false; + } + + //***************************************************************** + /// Clear all observers. + //***************************************************************** + ETL_CONSTEXPR14 void clear_observers() + { + for (size_t i = 0; i < Max_Observers; ++i) + { + delegate_list[i].clear(); + } + + delegate_count = 0; + } + + //***************************************************************** + /// Returns the number of observers. + //***************************************************************** + ETL_CONSTEXPR14 size_type number_of_observers() const + { + return delegate_count; + } + + //***************************************************************** + /// Notify all of the observers, sending them the notification. + ///\tparam TNotification The notification type. + ///\param n The notification. + //***************************************************************** + ETL_CONSTEXPR14 void notify_observers(notification_type n) const + { + if (delegate_count != 0) + { + for (size_t i = 0; i < Max_Observers; ++i) + { + delegate_list[i].call_if(n); + } + } + } + + private: + + /// The list of observers. + DelegateList delegate_list; + + /// The number of active delegates. + size_t delegate_count; + }; + + //************************************************************************* + /// Template deduction guides. + //************************************************************************* +#if ETL_USING_CPP17 + template + delegate_observable(TNotification, TDelegates...) -> delegate_observable; +#endif +} + +#endif diff --git a/include/etl/delegate_observer.h b/include/etl/delegate_observer.h deleted file mode 100644 index e2225cf9..00000000 --- a/include/etl/delegate_observer.h +++ /dev/null @@ -1,262 +0,0 @@ -///\file - -/****************************************************************************** -The MIT License(MIT) - -Embedded Template Library. -https://github.com/ETLCPP/etl -https://www.etlcpp.com - -Copyright(c) 2023 John Wellbelove - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files(the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions : - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -******************************************************************************/ - -#ifndef ETL_DELEGATE_OBSERVER_INCLUDED -#define ETL_DELEGATE_OBSERVER_INCLUDED - -#include "algorithm.h" - -#include "platform.h" -#include "delegate.h" -#include "vector.h" -#include "exception.h" -#include "error_handler.h" -#include "utility.h" - -namespace etl -{ - //*************************************************************************** - ///\ingroup observer - /// The base class for delegate observer exceptions. - //*************************************************************************** - class delegate_observer_exception : public exception - { - public: - - delegate_observer_exception(string_type reason_, string_type file_name_, numeric_type line_number_) - : exception(reason_, file_name_, line_number_) - { - } - }; - - //*************************************************************************** - ///\ingroup observer - /// The exception thrown when the delegate observer list is full. - //*************************************************************************** - class delegate_observer_list_full : public delegate_observer_exception - { - public: - - delegate_observer_list_full(string_type file_name_, numeric_type line_number_) - : delegate_observer_exception(ETL_ERROR_TEXT("delegate_observable:full", ETL_DELEGATE_OBSERVER_FILE_ID"A"), file_name_, line_number_) - { - } - }; - - //********************************************************************* - /// The object that is being observed. - ///\tparam MAX_OBSERVERS The maximum number of observers that can be accommodated. - ///\ingroup observer - //********************************************************************* - template - class delegate_observable - { - public: - - typedef etl::delegate observer_type; - - private: - - //*********************************** - // Item stored in the observer list. - //*********************************** - struct observer_item - { - observer_item(observer_type observer_) - : observer(observer_) - , enabled(true) - { - } - - observer_type observer; - bool enabled; - }; - - //*********************************** - // How to compare an observer with an observer list item. - //*********************************** - struct compare_observers - { - compare_observers(observer_type& observer_) - : observer(&observer_) - { - } - - bool operator ()(const observer_item& item) const - { - return observer == item.observer; - } - - observer_type observer; - }; - - typedef etl::vector Observer_List; - - public: - - typedef size_t size_type; - typedef TNotification notification_type; - - //***************************************************************** - /// Add an observer to the list. - /// If asserts or exceptions are enabled then an etl::observable_observer_list_full - /// is emitted if the observer list is already full. - ///\param observer A reference to the observer. - //***************************************************************** - void add_observer(observer_type& observer) - { - // See if we already have it in our list. - typename Observer_List::iterator i_observer_item = find_observer(observer); - - // Not there? - if (i_observer_item == observer_list.end()) - { - // Is there enough room? - ETL_ASSERT_OR_RETURN(!observer_list.full(), ETL_ERROR(etl::observer_list_full)); - - // Add it. - observer_list.push_back(observer_item(observer)); - } - } - - //***************************************************************** - /// Remove a particular observer from the list. - ///\param observer A reference to the observer. - ///\return true if the observer was removed, false if not. - //***************************************************************** - bool remove_observer(observer_type& observer) - { - // See if we have it in our list. - typename Observer_List::iterator i_observer_item = find_observer(observer); - - // Found it? - if (i_observer_item != observer_list.end()) - { - // Erase it. - observer_list.erase(i_observer_item); - return true; - } - else - { - return false; - } - } - - //***************************************************************** - /// Enable an observer - ///\param observer A reference to the observer. - ///\param state true to enable, false to disable. Default is enable. - //***************************************************************** - void enable_observer(observer_type& observer, bool state = true) - { - // See if we have it in our list. - typename Observer_List::iterator i_observer_item = find_observer(observer); - - // Found it? - if (i_observer_item != observer_list.end()) - { - i_observer_item->enabled = state; - } - } - - //***************************************************************** - /// Disable an observer - //***************************************************************** - void disable_observer(observer_type& observer) - { - // See if we have it in our list. - typename Observer_List::iterator i_observer_item = find_observer(observer); - - // Found it? - if (i_observer_item != observer_list.end()) - { - i_observer_item->enabled = false; - } - } - - //***************************************************************** - /// Clear all observers from the list. - //***************************************************************** - void clear_observers() - { - observer_list.clear(); - } - - //***************************************************************** - /// Returns the number of observers. - //***************************************************************** - size_type number_of_observers() const - { - return observer_list.size(); - } - - //***************************************************************** - /// Notify all of the observers, sending them the notification. - ///\tparam TNotification The notification type. - ///\param n The notification. - //***************************************************************** - void notify_observers(TNotification n) - { - typename Observer_List::iterator i_observer_item = observer_list.begin(); - - while (i_observer_item != observer_list.end()) - { - if (i_observer_item->enabled) - { - i_observer_item->p_observer->notification(n); - } - - ++i_observer_item; - } - } - - protected: - - ~observable() - { - } - - private: - - //***************************************************************** - /// Find an observer in the list. - /// Returns the end of the list if not found. - //***************************************************************** - typename Observer_List::iterator find_observer(observer_type& observer_) - { - return etl::find_if(observer_list.begin(), observer_list.end(), compare_observers(observer_)); - } - - /// The list of observers. - Observer_List observer_list; - }; -} - -#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 64d11735..0acc4c26 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -153,6 +153,7 @@ add_executable(etl_tests test_debounce.cpp test_delegate.cpp test_delegate_cpp03.cpp + test_delegate_observable.cpp test_delegate_service.cpp test_delegate_service_compile_time.cpp test_delegate_service_cpp03.cpp diff --git a/test/test_delegate_observable.cpp b/test/test_delegate_observable.cpp new file mode 100644 index 00000000..ec86eeca --- /dev/null +++ b/test/test_delegate_observable.cpp @@ -0,0 +1,264 @@ +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2025 John Wellbelove + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ + +#include "unit_test_framework.h" + +#include "etl/delegate.h" +#include "etl/delegate_observable.h" + +#if !defined(ETL_DELEGATE_FORCE_CPP03_IMPLEMENTATION) + +namespace +{ + const size_t Max_Observers = 2UL; + + using Notification = int; + using Observable = etl::delegate_observable; + using Delegate = Observable::delegate_type; + + //***************************************************************************** + bool global_called = false; + bool member_called = false; + int global_value = 0; + int member_value = 0; + + //***************************************************************************** + // The global function taking no parameters. + //***************************************************************************** + static void global(int id) + { + global_called = true; + global_value = id; + } + + //***************************************************************************** + // The test class with member functions. + //***************************************************************************** + class Object + { + public: + + ETL_CONSTEXPR14 Object() + { + } + + void member(int id) + { + member_called = true; + member_value = id; + } + }; + + Object object; + + // Callback for 'member1'. + ETL_CONSTEXPR14 Delegate member_callback = Delegate::create(); + + // Callback for 'global1'. + ETL_CONSTEXPR14 Delegate global_callback = Delegate::create(); + + //***************************************************************************** + // Initialises the test results. + //***************************************************************************** + struct SetupFixture + { + SetupFixture() + { + global_called = false; + member_called = false; + global_value = 0; + member_value = 0; + } + }; + + //************************************************************************* + ETL_CONSTEXPR14 Observable CreateObservable() + { + Observable observable; + + observable.add_observer(global_callback); + observable.add_observer(member_callback); + + observable.remove_observer(global_callback); + + return observable; + } +} + +namespace +{ + SUITE(test_delegate_service) + { + //************************************************************************* + TEST_FIXTURE(SetupFixture, test_constexpr_delegate_observable) + { + ETL_CONSTEXPR14 Observable observable(global_callback, member_callback); + + CHECK_EQUAL(2, observable.number_of_observers()); + + observable.notify_observers(1); + + CHECK_TRUE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(1, global_value); + CHECK_EQUAL(1, member_value); + + //********************************* + global_called = false; + member_called = false; + + observable.notify_observers(2); + + CHECK_TRUE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(2, global_value); + CHECK_EQUAL(2, member_value); + } + +#if ETL_USING_CPP17 + //************************************************************************* + TEST_FIXTURE(SetupFixture, test_constexpr_delegate_observable_using_template_deduction_guide_cpp17) + { + ETL_CONSTEXPR14 etl::delegate_observable observable(int{}, global_callback, member_callback); + + CHECK_EQUAL(2, observable.number_of_observers()); + + observable.notify_observers(1); + + CHECK_TRUE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(1, global_value); + CHECK_EQUAL(1, member_value); + + //********************************* + global_called = false; + member_called = false; + + observable.notify_observers(2); + + CHECK_TRUE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(2, global_value); + CHECK_EQUAL(2, member_value); + } +#endif + + //************************************************************************* + TEST_FIXTURE(SetupFixture, test_constexpr_delegate_observable_from_function) + { + ETL_CONSTEXPR14 Observable observable = CreateObservable(); + + CHECK_EQUAL(1, observable.number_of_observers()); + + observable.notify_observers(1); + + CHECK_FALSE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(0, global_value); + CHECK_EQUAL(1, member_value); + + //********************************* + global_called = false; + member_called = false; + + observable.notify_observers(2); + + CHECK_FALSE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(0, global_value); + CHECK_EQUAL(2, member_value); + } + + //************************************************************************* + TEST_FIXTURE(SetupFixture, test_delegate_observable_add_and_remove) + { + Observable observable; + CHECK_EQUAL(0, observable.number_of_observers()); + + observable.notify_observers(1); + CHECK_FALSE(global_called); + CHECK_FALSE(member_called); + CHECK_EQUAL(0, global_value); + CHECK_EQUAL(0, member_value); + + //********************************* + global_called = false; + member_called = false; + + observable.add_observer(global_callback); + CHECK_EQUAL(1, observable.number_of_observers()); + + observable.notify_observers(2); + CHECK_TRUE(global_called); + CHECK_FALSE(member_called); + CHECK_EQUAL(2, global_value); + CHECK_EQUAL(0, member_value); + + //********************************* + global_called = false; + member_called = false; + + observable.add_observer(member_callback); + CHECK_EQUAL(2, observable.number_of_observers()); + + observable.notify_observers(3); + CHECK_TRUE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(3, global_value); + CHECK_EQUAL(3, member_value); + + //********************************* + global_called = false; + member_called = false; + + observable.remove_observer(global_callback); + CHECK_EQUAL(1, observable.number_of_observers()); + + observable.notify_observers(4); + CHECK_FALSE(global_called); + CHECK_TRUE(member_called); + CHECK_EQUAL(3, global_value); + CHECK_EQUAL(4, member_value); + + //********************************* + global_called = false; + member_called = false; + + observable.clear_observers(); + CHECK_EQUAL(0, observable.number_of_observers()); + + observable.notify_observers(5); + CHECK_FALSE(global_called); + CHECK_FALSE(member_called); + CHECK_EQUAL(3, global_value); + CHECK_EQUAL(4, member_value); + } + }; +} + +#endif diff --git a/test/vs2022/etl.vcxproj b/test/vs2022/etl.vcxproj index 445f32d6..60bad33d 100644 --- a/test/vs2022/etl.vcxproj +++ b/test/vs2022/etl.vcxproj @@ -3353,6 +3353,7 @@ + @@ -9387,6 +9388,7 @@ + diff --git a/test/vs2022/etl.vcxproj.filters b/test/vs2022/etl.vcxproj.filters index db94bfea..485612d0 100644 --- a/test/vs2022/etl.vcxproj.filters +++ b/test/vs2022/etl.vcxproj.filters @@ -1530,6 +1530,9 @@ UnitTest++\Header Files + + ETL\Utilities + @@ -3689,6 +3692,9 @@ Tests\CRC + + Tests\Callbacks & Delegates + @@ -3847,6 +3853,12 @@ Resource Files + + Tests + + + Tests + @@ -3885,6 +3897,7 @@ Tests\Syntax Checks +