├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── tests ├── CMakeLists.txt └── signal.test.cpp ├── .clang-format ├── include └── signals_light │ └── signal.hpp └── docs └── design.md /.gitignore: -------------------------------------------------------------------------------- 1 | build*/ 2 | .clangd/ 3 | .cache/ 4 | compile_commands.json 5 | tags 6 | *.[ao] 7 | *.so 8 | *.out 9 | *.vim 10 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(SignalsLight LANGUAGES CXX) 3 | 4 | add_library(signals-light INTERFACE) 5 | 6 | target_include_directories(signals-light 7 | INTERFACE 8 | ${CMAKE_CURRENT_SOURCE_DIR}/include 9 | ) 10 | 11 | target_compile_features(signals-light 12 | INTERFACE 13 | cxx_std_17 14 | ) 15 | 16 | include(GNUInstallDirs) 17 | install( 18 | DIRECTORY 19 | ${PROJECT_SOURCE_DIR}/include/signals_light 20 | DESTINATION 21 | ${CMAKE_INSTALL_INCLUDEDIR} 22 | ) 23 | 24 | add_subdirectory(tests) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 Anthony Leedom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signals Light 🪶 2 | 3 | This is a Signals and Slots library. The `Signal` class is an observer type, it 4 | can have `Slots`(functions) registered to it, and when the Signal is emitted, 5 | all registered Slots are invoked. It is written in C++17 and is header only. 6 | 7 | Slots can track the lifetime of particular objects and they will disable 8 | themselves when one of the tracked objects is destroyed. This is useful if the 9 | function you are registering with the Signal holds a reference to an object that 10 | might be destroyed before the Signal is destroyed. 11 | 12 | This library is 'light' in terms of the boost::Signal2 library, which is thread 13 | safe, has ordered connections, and in general is more feature-heavy. 14 | 15 | ```cpp 16 | #include 17 | 18 | { 19 | auto signal = sl::Signal{}; 20 | signal.connect([](int i){ std::cout << i << '\n'; }); 21 | signal.connect([](int i){ std::cout << i * 2 << '\n'; }); 22 | 23 | signal(4); // prints "4\n8\n" to standard output. 24 | } 25 | ``` 26 | 27 | ## Getting Started 28 | 29 | The `CMakeLists.txt` file will give you a `signals-light` library target to link against in your own project. 30 | 31 | - `#include ` 32 | - namespace `sl`. -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Test Library Dependency 2 | include(FetchContent) 3 | FetchContent_Declare( 4 | zzz 5 | GIT_REPOSITORY https://github.com/a-n-t-h-o-n-y/zzz.git 6 | GIT_TAG 9d7c047f47c81a95a5ea824075253618356593a2 7 | ) 8 | FetchContent_MakeAvailable(zzz) 9 | 10 | add_executable(signals_light.tests.unit EXCLUDE_FROM_ALL 11 | signal.test.cpp 12 | ) 13 | 14 | target_link_libraries(signals_light.tests.unit 15 | PRIVATE 16 | signals-light 17 | zzz 18 | ) 19 | 20 | target_compile_options(signals_light.tests.unit 21 | PRIVATE 22 | -Wall 23 | -Wextra 24 | -Wpedantic 25 | ) 26 | 27 | message(STATUS "AddressSanitizer Enabled for signals_light.tests.unit") 28 | if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") 29 | target_compile_options(signals_light.tests.unit 30 | PRIVATE 31 | -fsanitize=address 32 | -fno-omit-frame-pointer 33 | ) 34 | target_link_options(signals_light.tests.unit 35 | PRIVATE 36 | -fsanitize=address 37 | ) 38 | elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") 39 | target_compile_options(signals_light.tests.unit 40 | PRIVATE 41 | /fsanitize=address 42 | ) 43 | target_link_options(signals_light.tests.unit 44 | PRIVATE 45 | /INCREMENTAL:NO 46 | /DEBUG 47 | /fsanitize=address 48 | ) 49 | endif() -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Chromium 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: true 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortBlocksOnASingleLine: true 13 | AllowShortCaseLabelsOnASingleLine: true 14 | AllowShortFunctionsOnASingleLine: All 15 | AllowShortIfStatementsOnASingleLine: Never 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: true 20 | AlwaysBreakTemplateDeclarations: Yes 21 | BinPackArguments: true 22 | BinPackParameters: false 23 | BraceWrapping: # Based on Stroustrup 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: true 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | AfterExternBlock: true 33 | BeforeCatch: true 34 | BeforeElse: true 35 | IndentBraces: false 36 | SplitEmptyFunction: false 37 | SplitEmptyRecord: false 38 | SplitEmptyNamespace: false 39 | BreakBeforeBinaryOperators: None 40 | BreakBeforeBraces: Custom 41 | BreakBeforeInheritanceComma: false 42 | BreakInheritanceList: BeforeColon 43 | BreakBeforeTernaryOperators: true 44 | BreakConstructorInitializersBeforeComma: false 45 | BreakConstructorInitializers: BeforeColon 46 | BreakAfterJavaFieldAnnotations: false 47 | BreakStringLiterals: true 48 | ColumnLimit: 80 49 | # CommentPragmas: '' 50 | CompactNamespaces: false 51 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 52 | ConstructorInitializerIndentWidth: 4 53 | ContinuationIndentWidth: 4 54 | Cpp11BracedListStyle: true 55 | DerivePointerAlignment: false 56 | DisableFormat: false 57 | ExperimentalAutoDetectBinPacking: false 58 | FixNamespaceComments: true 59 | ForEachMacros: 60 | - foreach 61 | - Q_FOREACH 62 | - BOOST_FOREACH 63 | IncludeBlocks: Preserve 64 | IncludeCategories: 65 | - Regex: '^' 66 | Priority: 2 67 | - Regex: '^<.*\.h>' 68 | Priority: 1 69 | - Regex: '^<.*' 70 | Priority: 2 71 | - Regex: '.*' 72 | Priority: 3 73 | IncludeIsMainRegex: '([-_](test|unittest))?$' 74 | IndentCaseLabels: true 75 | IndentPPDirectives: AfterHash 76 | IndentWidth: 4 77 | IndentWrappedFunctionNames: false 78 | JavaScriptQuotes: Leave 79 | JavaScriptWrapImports: true 80 | KeepEmptyLinesAtTheStartOfBlocks: false 81 | MacroBlockBegin: '' 82 | MacroBlockEnd: '' 83 | MaxEmptyLinesToKeep: 1 84 | NamespaceIndentation: None 85 | ObjCBinPackProtocolList: Never 86 | ObjCBlockIndentWidth: 2 87 | ObjCSpaceAfterProperty: false 88 | ObjCSpaceBeforeProtocolList: true 89 | PenaltyBreakAssignment: 2 90 | PenaltyBreakBeforeFirstCallParameter: 1 91 | PenaltyBreakComment: 300 92 | PenaltyBreakFirstLessLess: 120 93 | PenaltyBreakString: 1000 94 | PenaltyBreakTemplateDeclaration: 10 95 | PenaltyExcessCharacter: 1000000 96 | PenaltyReturnTypeOnItsOwnLine: 200 97 | PointerAlignment: Left 98 | RawStringFormats: 99 | - Language: Cpp 100 | Delimiters: 101 | - cc 102 | - CC 103 | - cpp 104 | - Cpp 105 | - CPP 106 | - 'c++' 107 | - 'C++' 108 | CanonicalDelimiter: '' 109 | BasedOnStyle: google 110 | - Language: TextProto 111 | Delimiters: 112 | - pb 113 | - PB 114 | - proto 115 | - PROTO 116 | EnclosingFunctions: 117 | - EqualsProto 118 | - EquivToProto 119 | - PARSE_PARTIAL_TEXT_PROTO 120 | - PARSE_TEST_PROTO 121 | - PARSE_TEXT_PROTO 122 | - ParseTextOrDie 123 | - ParseTextProtoOrDie 124 | CanonicalDelimiter: '' 125 | BasedOnStyle: google 126 | ReflowComments: true 127 | SortIncludes: true 128 | SortUsingDeclarations: true 129 | SpaceAfterCStyleCast: false 130 | SpaceAfterTemplateKeyword: true 131 | SpaceBeforeAssignmentOperators: true 132 | SpaceBeforeCpp11BracedList: false 133 | SpaceBeforeCtorInitializerColon: true 134 | SpaceBeforeInheritanceColon: true 135 | SpaceBeforeParens: ControlStatements 136 | SpaceBeforeRangeBasedForLoopColon: true 137 | SpaceInEmptyParentheses: false 138 | SpacesBeforeTrailingComments: 2 139 | SpacesInAngles: false 140 | SpacesInContainerLiterals: true 141 | SpacesInCStyleCastParentheses: false 142 | SpacesInParentheses: false 143 | SpacesInSquareBrackets: false 144 | Standard: Cpp11 145 | StatementMacros: 146 | - Q_UNUSED 147 | - QT_REQUIRE_VERSION 148 | TabWidth: 4 149 | UseTab: Never 150 | ... 151 | -------------------------------------------------------------------------------- /tests/signal.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define TEST_MAIN 6 | #include 7 | 8 | #include 9 | 10 | TEST(signal_no_slots) 11 | { 12 | { // void Signal return type will return void 13 | { 14 | auto sig = sl::Signal{}; 15 | ASSERT((std::is_same_v)); 16 | } 17 | { 18 | auto sig = sl::Signal{}; 19 | ASSERT((std::is_same_v)); 20 | } 21 | } 22 | 23 | { // non-void Signal return type will return std::nullopt 24 | { 25 | auto sig = sl::Signal{}; 26 | ASSERT((std::is_same_v>)); 27 | ASSERT(sig() == std::nullopt); 28 | } 29 | { 30 | auto sig = sl::Signal{}; 31 | ASSERT((std::is_same_v>)); 33 | ASSERT(sig(1, 'a', 3.14f) == std::nullopt); 34 | } 35 | } 36 | } 37 | 38 | TEST(slot_connect_and_disconnect) 39 | { 40 | { // Returns the value of the connected Slot when only one Slot connected 41 | auto sig = sl::Signal{}; 42 | ASSERT(sig.is_empty()); 43 | ASSERT(sig.slot_count() == 0); 44 | ASSERT(sig() == std::nullopt); 45 | 46 | auto const id = sig.connect([] { return 5; }); 47 | ASSERT((std::is_same_v)); 48 | 49 | ASSERT(!sig.is_empty()); 50 | ASSERT(sig.slot_count() == 1); 51 | 52 | auto const result = sig(); 53 | ASSERT((std::is_same_v const>)); 54 | ASSERT(result.has_value()); 55 | ASSERT(*result == 5); 56 | 57 | sig.disconnect(id); 58 | ASSERT(sig.is_empty()); 59 | ASSERT(sig.slot_count() == 0); 60 | ASSERT(sig() == std::nullopt); 61 | } 62 | 63 | { // Last connected Slot's return value is returned. 64 | auto sig = sl::Signal{}; 65 | auto const id_1 = sig.connect([] { return 5; }); 66 | ASSERT((std::is_same_v)); 67 | 68 | sig.connect([] { return 3; }); 69 | ASSERT(sig.slot_count() == 2); 70 | 71 | auto const result = sig(); 72 | ASSERT((std::is_same_v const>)); 73 | ASSERT(result.has_value()); 74 | ASSERT(*result == 3); 75 | 76 | // Disconnecting Slot 1; will still return Slot 2's return value. 77 | sig.disconnect(id_1); 78 | ASSERT(!sig.is_empty()); 79 | ASSERT(sig.slot_count() == 1); 80 | 81 | auto const result_2 = sig(); 82 | ASSERT((std::is_same_v const>)); 83 | ASSERT(result_2.has_value()); 84 | ASSERT(*result_2 == 3); 85 | } 86 | 87 | { // Last connected Slot's return value is returned. 88 | auto sig = sl::Signal{}; 89 | sig.connect([] { return 5; }); 90 | 91 | auto const id_2 = sig.connect([] { return 3; }); 92 | ASSERT(sig.slot_count() == 2); 93 | 94 | auto const result = sig(); 95 | ASSERT((std::is_same_v const>)); 96 | ASSERT(result.has_value()); 97 | ASSERT(*result == 3); 98 | 99 | // Disconnecting Slot 2; will now return Slot 1's return value. 100 | sig.disconnect(id_2); 101 | ASSERT(!sig.is_empty()); 102 | ASSERT(sig.slot_count() == 1); 103 | 104 | auto const result_2 = sig(); 105 | ASSERT((std::is_same_v const>)); 106 | ASSERT(result_2.has_value()); 107 | ASSERT(*result_2 == 5); 108 | } 109 | } 110 | 111 | TEST(emitting_signal_invokes_slot_fns) 112 | { 113 | auto sig = sl::Signal{}; 114 | 115 | sig.connect([](int a, int b, int c) { return a + b + c; }); 116 | ASSERT(sig(5, 4, 3).value() == 12); 117 | 118 | sig.connect([](int a, int b, int c) { return a * b * c; }); 119 | ASSERT(sig(5, 4, 3).value() == 60); 120 | } 121 | 122 | TEST(disconnecting_with_invalid_id_will_throw) 123 | { 124 | auto sig = sl::Signal{}; 125 | auto const id = sig.connect([] {}); 126 | sig.disconnect(id); 127 | 128 | ASSERT_THROWS(sig.disconnect(id), std::invalid_argument); 129 | ASSERT_THROWS(sig.disconnect(sl::Identifier{}), std::invalid_argument); 130 | } 131 | 132 | TEST(lifetime_tracking) 133 | { 134 | { // Destroying the only tracked object on a Slot will disable that Slot. 135 | auto sig = sl::Signal{}; 136 | auto slot = sl::Slot{[] { return 5; }}; 137 | { 138 | auto life = sl::Lifetime{}; 139 | slot.track(life); 140 | sig.connect(slot); 141 | ASSERT(*sig() == 5); 142 | } 143 | ASSERT(sig() == std::nullopt); 144 | } 145 | 146 | { // Lifetime tracking only works if track() is called before connect(). 147 | auto sig = sl::Signal{}; 148 | auto slot = sl::Slot{[] { return 5; }}; 149 | sig.connect(slot); 150 | { 151 | auto life = sl::Lifetime{}; 152 | slot.track(life); 153 | ASSERT(*sig() == 5); 154 | } 155 | ASSERT(*sig() == 5); 156 | } 157 | 158 | { // Only a single tracked object needs to be destroyed to disable a Slot. 159 | auto sig = sl::Signal{}; 160 | auto slot = sl::Slot{[] { return 5; }}; 161 | auto life_1 = sl::Lifetime{}; 162 | slot.track(life_1); 163 | { 164 | auto life_2 = sl::Lifetime{}; 165 | slot.track(life_2); 166 | sig.connect(slot); 167 | ASSERT(*sig() == 5); 168 | } 169 | ASSERT(sig() == std::nullopt); 170 | } 171 | 172 | { // Non-disabled Slots will still be run. 173 | auto sig = sl::Signal{}; 174 | auto slot_1 = sl::Slot{[] { return 5; }}; 175 | sig.connect(slot_1); 176 | { 177 | auto slot_2 = sl::Slot{[] { return 3; }}; 178 | auto life = sl::Lifetime{}; 179 | slot_2.track(life); 180 | sig.connect(slot_2); 181 | ASSERT(*sig() == 3); 182 | } 183 | ASSERT(*sig() == 5); 184 | } 185 | } -------------------------------------------------------------------------------- /include/signals_light/signal.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIGNALS_LIGHT_SIGNAL_HPP 2 | #define SIGNALS_LIGHT_SIGNAL_HPP 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace sl { 14 | 15 | /// Provides a const view of a std::weak_ptr, providing an is_expired() check. 16 | /** Always contains a valid lifetime, unless moved from. */ 17 | class Lifetime_observer { 18 | public: 19 | Lifetime_observer() = delete; 20 | 21 | // Note: The below overloads are provided so a Lifetime_observer can be 22 | // implicitly constructed from std::..._ptr objects in a function parameter. 23 | 24 | /// Construct a view of the lifetime of the object pointed to by \p p. 25 | /** Throws std::invalid_argument if \p p is nullptr. */ 26 | template 27 | Lifetime_observer(std::weak_ptr p) noexcept(false) : handle_{sanitize(p)} 28 | {} 29 | 30 | /// Construct a view of the lifetime of the object pointed to by \p p. 31 | /** Throws std::invalid_argument if \p p is nullptr. */ 32 | template 33 | Lifetime_observer(std::shared_ptr p) noexcept(false) 34 | : handle_{sanitize(p)} 35 | {} 36 | 37 | /// Creates a new observer to the same object. 38 | Lifetime_observer(Lifetime_observer const&) = default; 39 | 40 | /// Moving will leave the moved from Lifetime_observer in an undefined state 41 | Lifetime_observer(Lifetime_observer&&) = default; 42 | 43 | /// Overwrites *this to track the same lifetime as the rhs. 44 | auto operator=(Lifetime_observer const&) -> Lifetime_observer& = default; 45 | 46 | /// Moving will leave the moved from Lifetime_observer in an undefined state 47 | auto operator=(Lifetime_observer&&) -> Lifetime_observer& = default; 48 | 49 | public: 50 | /// Return true if the tracked object has been deleted. 51 | auto is_expired() const noexcept -> bool { return handle_.expired(); } 52 | 53 | /// Return a unique id that is associated with the tracked object 54 | /** Returns zero if the tracked object has been destroyed. */ 55 | auto get_id() const noexcept -> std::uintptr_t 56 | { 57 | if (this->is_expired()) 58 | return 0; 59 | return reinterpret_cast(handle_.lock().get()); 60 | } 61 | 62 | private: 63 | std::weak_ptr handle_; 64 | 65 | private: 66 | /// Throws std::invalid_argument if p == nullptr, returns \p p otherwise. 67 | template 68 | static auto sanitize(Pointer p) noexcept(false) -> Pointer 69 | { 70 | auto constexpr message = "Lifetime_observer: Can't observer a nullptr."; 71 | return p == nullptr ? throw std::invalid_argument{message} : p; 72 | } 73 | }; 74 | 75 | /// A class to keep track of an object's lifetime. 76 | /** A Lifetime_observer can check if a Lifetime has ended. */ 77 | class Lifetime { 78 | public: 79 | /// Create a new lifetime to track. 80 | Lifetime() noexcept(false) : life_{std::make_shared(true)} {} 81 | 82 | /// Create a new lifetime to track. 83 | /** Tracking does not split across multiple Lifetime objects. */ 84 | Lifetime(Lifetime const&) noexcept(false) 85 | : life_{std::make_shared(true)} 86 | {} 87 | 88 | /// Transfers the lifetime tracking to the new instance. 89 | /** Existing trackers will now track the newly constructed lifetime. */ 90 | Lifetime(Lifetime&&) = default; 91 | 92 | /// Create a new lifetime to track, destroying the existing lifetime. 93 | /** Tracking does not split across multiple Lifetime objects. */ 94 | auto operator=(Lifetime const& rhs) noexcept(false) -> Lifetime& 95 | { 96 | if (this == &rhs) 97 | return *this; 98 | life_ = std::make_shared(true); 99 | return *this; 100 | } 101 | 102 | /// Transfers the lifetime of the moved from object to *this. 103 | /** Anything tracking the moved from object will now be tracking *this. */ 104 | auto operator=(Lifetime&& rhs) noexcept -> Lifetime& 105 | { 106 | if (this == &rhs) 107 | return *this; 108 | life_ = std::move(rhs.life_); 109 | return *this; 110 | } 111 | 112 | public: 113 | /// Return a Lifetime_observer, to check if *this has been destroyed. 114 | /** The returned object is valid even after *this i\ destroyed. Even though 115 | * this should never throw an exception, if a memory allocation fails at 116 | * construction, it might not throw std::bad_alloc, returning nullptr. */ 117 | auto track() const noexcept(false) -> Lifetime_observer { return life_; } 118 | 119 | private: 120 | std::shared_ptr life_; 121 | }; 122 | 123 | template 124 | class Slot; 125 | 126 | /// An invocable type with object lifetime tracking. 127 | /** An object of this type always holds a valid std::function object. 128 | * The call operator will throw if any of the tracked objects are destroyed. */ 129 | template 130 | class Slot { 131 | public: 132 | using Signature_t = R(Args...); 133 | using Function_t = std::function; 134 | 135 | /// Exception thrown if any object being tracked has been destroyed. 136 | class Expired {}; 137 | 138 | public: 139 | Slot() = delete; 140 | 141 | Slot(std::nullptr_t) = delete; 142 | 143 | /// Construct a Slot with the slot function \p f and no tracked objects. 144 | /** F must be invocable with Args... and return convertible to R. */ 145 | template 146 | Slot(F f) : f_{f} 147 | { 148 | static_assert(std::is_invocable_r_v, 149 | "Slot initialization with invalid function type."); 150 | } 151 | 152 | /// Construct a Slot with the slot function \p f and no tracked objects. 153 | /** Throws std::invalid_argument if the given function is empty. */ 154 | Slot(Function_t f) noexcept(false) : f_{sanitize(std::move(f))} {} 155 | 156 | /// Copies the slot function and the tracked list into the new Slot. 157 | /** Tracked objects by *this are tracked by the new Slot instance. */ 158 | Slot(Slot const&) = default; 159 | 160 | /// Moves the slot function and the tracked list into the new Slot. 161 | Slot(Slot&&) = default; 162 | 163 | /// Copies the slot function and the tracked object reference container. 164 | auto operator=(Slot const&) -> Slot& = default; 165 | 166 | /// Moves the slot function and the tracked object reference container. 167 | auto operator=(Slot&&) -> Slot& = default; 168 | 169 | public: 170 | /// Track any object owned by a std::shared_ptr<...>. 171 | /** Returns *this. Tracking the same item multiple times is not checked. */ 172 | auto track(Lifetime_observer const& x) noexcept(false) -> Slot& 173 | { 174 | observers_.push_back(x); 175 | return *this; 176 | } 177 | 178 | /// Track Lifetime object. Convenience. 179 | /** Returns *this. Tracking the same item multiple times is not checked. */ 180 | auto track(Lifetime const& x) noexcept(false) -> Slot& 181 | { 182 | this->track(x.track()); 183 | return *this; 184 | } 185 | 186 | /// Remove the observed object from the tracked objects list. 187 | /** Removes on Lifetime_observer::get_id() equality. Returns *this. Throws 188 | * std::invalid_argument if \p x is not being tracked by *this. */ 189 | auto untrack(Lifetime_observer const& x) noexcept(false) -> Slot& 190 | { 191 | auto const iter = 192 | std::find_if(std::cbegin(observers_), std::cend(observers_), 193 | [&x](Lifetime_observer const& y) { 194 | return x.get_id() == y.get_id(); 195 | }); 196 | if (iter == std::cend(observers_)) 197 | throw std::invalid_argument{"Slot::untrack: x not found."}; 198 | observers_.erase(iter); 199 | return *this; 200 | } 201 | 202 | /// Remove the given Lifetime object from the tracked objects list. 203 | /** Removes on Lifetime_observer::get_id() equality. Returns *this. Throws 204 | * std::invalid_argument if \p x is not being tracked by *this.*/ 205 | auto untrack(Lifetime const& x) noexcept(false) -> Slot& 206 | { 207 | return this->untrack(x.track()); 208 | } 209 | 210 | /// Invokes the internally held function 211 | /** Throws Expired if any tracked object has been destroyed. */ 212 | template 213 | auto operator()(Arguments&&... args) const noexcept(false) -> R 214 | { 215 | if (is_expired()) 216 | throw Expired{}; 217 | return f_(std::forward(args)...); 218 | } 219 | 220 | /// Check whether any of the tracked lifetimes has expired. 221 | /** Returns true if no lifetimes are being tracked. */ 222 | auto is_expired() const noexcept -> bool 223 | { 224 | return std::any_of( 225 | std::cbegin(observers_), std::cend(observers_), 226 | [](Lifetime_observer const& x) { return x.is_expired(); }); 227 | } 228 | 229 | /// Return a const reference to the internal std::function. 230 | /** Always returns a valid Function_t object that can be called. */ 231 | auto slot_function() const noexcept -> Function_t const& { return f_; } 232 | 233 | private: 234 | Function_t f_; 235 | std::vector observers_; 236 | 237 | private: 238 | /// Throws std::invalid_argument if f == nullptr, returns \p f otherwise. 239 | static auto sanitize(Function_t f) noexcept(false) -> Function_t 240 | { 241 | auto constexpr message = "Slot must be initialized with valid function"; 242 | return f ? std::move(f) : throw std::invalid_argument{message}; 243 | } 244 | }; 245 | 246 | /// Objects of this type can be unique and compared against other identifiers. 247 | class Identifier { 248 | public: 249 | using Underlying_int = std::uint32_t; 250 | 251 | public: 252 | /// Construct the initial value. 253 | Identifier() noexcept : value_{0} {} 254 | 255 | /// Generate the next identifier value, this is an increment. 256 | static auto next(Identifier x) noexcept -> Identifier 257 | { 258 | return {x.value_ + 1}; 259 | } 260 | 261 | public: 262 | /// Return true if both Identifiers have the same internal value. 263 | friend auto operator==(Identifier x, Identifier y) noexcept -> bool 264 | { 265 | return x.value_ == y.value_; 266 | } 267 | 268 | /// Return true if both Identifiers do not have the same internal value. 269 | friend auto operator!=(Identifier x, Identifier y) noexcept -> bool 270 | { 271 | return !(x == y); 272 | } 273 | 274 | private: 275 | /// Used by next(...). 276 | Identifier(Underlying_int value) noexcept : value_{value} {} 277 | 278 | private: 279 | Underlying_int value_; 280 | }; 281 | 282 | template 283 | class Signal; 284 | 285 | /// An observer type that calls registered callbacks(Slots) when emitted. 286 | template 287 | class Signal { 288 | public: 289 | using Signature_t = R(Args...); 290 | using Emit_result_t = 291 | std::conditional_t, void, std::optional>; 292 | 293 | public: 294 | /// Construct a Signal with no connected Slots. 295 | Signal() = default; 296 | 297 | /// Construct a Signal with the given Slot connected. 298 | Signal(Slot slot) { this->connect(std::move(slot)); } 299 | 300 | /// Construct a Signal with the given Slot function connected. 301 | /// Seems to be the only way to pass a lambda in certain contexts. 302 | template 303 | Signal(F slot_fn) 304 | { 305 | static_assert(std::is_invocable_r_v, 306 | "Slot initialization with invalid function type."); 307 | this->connect(Slot{std::move(slot_fn)}); 308 | } 309 | 310 | /// Create a Signal with the same Slots connected, and the same Identifiers. 311 | Signal(Signal const&) = default; 312 | 313 | /// Move the connected Slots from the existing Signal to the new one. 314 | /** The moved from Signal will be empty afterwards. */ 315 | Signal(Signal&&) = default; 316 | 317 | /// Overwrite the existing Signal with the Slots and Identifiers of the rhs. 318 | auto operator=(Signal const&) -> Signal& = default; 319 | 320 | /// Overwrite the existing Signal with the Slots and Identifiers of the rhs. 321 | /** The moved from Signal will be empty afterwards. */ 322 | auto operator=(Signal&&) -> Signal& = default; 323 | 324 | public: 325 | /// Invoke all non-expired Slots. 326 | /** Returns the return value of the last connected Slot or std::nullopt if 327 | * none. Expired Slots are ignored, rather than throwing an exception. */ 328 | auto emit(Args const&... args) const -> Emit_result_t 329 | { 330 | if constexpr (std::is_same_v) { 331 | for (auto const& [id, slot] : slots_) { 332 | if (slot.is_expired()) 333 | continue; 334 | slot.slot_function()(args...); 335 | } 336 | } 337 | else { 338 | // Only return the last non-expired slot result. 339 | auto const last_valid_iter = 340 | std::find_if(std::crbegin(slots_), std::crend(slots_), 341 | [](auto const& id_slot) { 342 | return !id_slot.second.is_expired(); 343 | }); 344 | if (last_valid_iter == std::crend(slots_)) 345 | return std::nullopt; 346 | for (auto const& [id, slot] : slots_) { 347 | if (slot.is_expired()) 348 | continue; 349 | if (&slot == &(last_valid_iter->second)) 350 | return slot.slot_function()(args...); 351 | slot.slot_function()(args...); 352 | } 353 | return std::nullopt; 354 | } 355 | } 356 | 357 | /// Alternative notation for Signal::emit. 358 | auto operator()(Args const&... args) const -> Emit_result_t 359 | { 360 | return this->emit(args...); 361 | } 362 | 363 | /// Register a Slot with *this, will be invoked when *this is emitted. 364 | /** Returns a unique Identifier, to be used with Signal::disconnect. */ 365 | auto connect(Slot s) noexcept(false) -> Identifier 366 | { 367 | auto const id = slots_.empty() ? Identifier{} 368 | : Identifier::next(slots_.back().first); 369 | slots_.push_back({id, std::move(s)}); 370 | return id; 371 | } 372 | 373 | /// Removes and returns the Slot associated with the given Identifier. 374 | /** Throws std::invalid_argument if no connected Slot is found with id. */ 375 | auto disconnect(Identifier id) noexcept(false) -> Slot 376 | { 377 | auto const iter = std::find_if( 378 | std::cbegin(slots_), std::cend(slots_), 379 | [id](auto const& id_slot) { return id_slot.first == id; }); 380 | if (iter == std::cend(slots_)) 381 | throw std::invalid_argument{"Signal::disconnect: No matching id."}; 382 | auto slot = std::move(iter->second); 383 | slots_.erase(iter); 384 | return std::move(slot); 385 | } 386 | 387 | /// Return the number of connected Slots. 388 | auto slot_count() const noexcept -> std::size_t { return slots_.size(); } 389 | 390 | /// Return true if there are no connected Slots. 391 | auto is_empty() const noexcept -> bool { return slots_.empty(); } 392 | 393 | private: 394 | std::vector>> slots_; 395 | }; 396 | 397 | } // namespace sl 398 | #endif // SIGNALS_LIGHT_SIGNAL_HPP 399 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Signals Light Design Document 2 | 3 | ## Name 4 | 5 | signals-light🪶 6 | 7 | ## Namespace 8 | 9 | `namespace sl {}` 10 | 11 | ## Files 12 | 13 | `include/signals_light/signal.hpp` 14 | 15 | ## Description 16 | 17 | This is a Signals and Slots library. The `Signal` class is an observer type, it 18 | can have `Slots`(functions) registered to it, and when the Signal is emitted, 19 | all registered Slots are invoked. 20 | 21 | Slots can track the lifetime of particular objects and they will disable 22 | themselves when one of the tracked objects is destroyed. This is useful if the 23 | function you are registering with the Signal holds a reference to an object that 24 | might be destroyed before the Signal is destroyed. 25 | 26 | This library is 'light' in terms of the boost::Signal2 library, which is thread 27 | safe, has ordered connections, and in general is more heavy-weight. 28 | 29 | ## Rationale 30 | 31 | The current signals library is too heavy to have many signals within each 32 | Widget, it is the largest source of stack size within the Widget class. If the 33 | size of Widget can be brought down, more Widgets can fit in a single cache line, 34 | potentially speeding up access to Widgets and speeding up UI code in CPPurses. 35 | 36 | Most of the features of the current signals library are not being utilized, 37 | thread safety is already guaranteed by the event system, therefore Signals do 38 | not need to worry about Widget data being changed out from under them. Grouping 39 | connected Slots and ordering is not used, as far as I know, execution order of 40 | Slots should not matter, Slots should be independent, if they are dependent, 41 | then they can be combined into a single slot. 42 | 43 | Current `Signal` size: **168 Bytes** 44 | 45 | Expected light `Signal` size: **24 Bytes** 46 | 47 | A reduction of **144 Bytes** 48 | 49 | There are a potential minimum of 36 Signals in a Widget, this number will go 50 | down without the move event, to 34 Widgets, once all filter signals are added. 51 | Extended Widgets may add more signals on top of this number. That is **6,048 52 | Bytes** solely for Signals, this will go down to **864 Bytes**, a reduction of 53 | **5,184 Bytes** per Widget. Depending on how many Widgets are in a single 54 | application, this is considerable. 55 | 56 | An L1 cache line is around 256 kB, in the current state that holds 42 Widgets, 57 | with the new design that is 296 Widgets. These will not be the only objects in 58 | the cache, but it leaves more space for others. 59 | 60 | ## Interfaces 61 | 62 | ### `class Lifetime_observer` 63 | 64 | This is esentailly a wrapper around `std::weak_ptr` that only allows 65 | access to the `is_expired()` method. Any non-const `std::weak_ptr` can reset the 66 | owned object, so using a `std::weak_ptr` is not ideal for the below 67 | `Lifetime_tracker` class since it would allow a client to invalidate the 68 | invariant that the `Lifetime_tracker` object should not be expired before it is 69 | destroyed. 70 | 71 | ```cpp 72 | /// Provides a const view of a std::weak_ptr, providing an is_expired() check. 73 | /** Always contains a valid lifetime, unless moved from. */ 74 | class Lifetime_observer { 75 | public: 76 | Lifetime_observer() = delete; 77 | 78 | // Note: The below overloads are provided so a Lifetime_observer can be 79 | // implicitly constructed from std::..._ptr objects in a function parameter. 80 | 81 | /// Construct a view of the lifetime of the object pointed to by \p p. 82 | /** Throws std::invalid_argument if \p p is nullptr. */ 83 | template 84 | Lifetime_observer(std::weak_ptr p); 85 | 86 | /// Construct a view of the lifetime of the object pointed to by \p p. 87 | /** Throws std::invalid_argument if \p p is nullptr. */ 88 | template 89 | Lifetime_observer(std::shared_ptr p); 90 | 91 | /// Creates a new observer to the same object. 92 | Lifetime_observer(Lifetime_observer const&) = default; 93 | 94 | /// Moving will leave the moved from Lifetime_observer in an undefined state 95 | Lifetime_observer(Lifetime_observer&&) = default; 96 | 97 | /// Overwrites *this to track the same lifetime as the rhs. 98 | auto operator=(Lifetime_observer const&) -> Lifetime_observer& = default; 99 | 100 | /// Moving will leave the moved from Lifetime_observer in an undefined state 101 | auto operator=(Lifetime_observer&&) -> Lifetime_observer& = default; 102 | 103 | public: 104 | /// Return true if the tracked object has been deleted. 105 | auto is_expired() const -> bool; 106 | 107 | /// Return a unique id that is associated with the tracked object 108 | /** Returns zero if the tracked object has been destroyed. */ 109 | auto get_id() const -> std::uintptr_t; 110 | 111 | private: 112 | std::weak_ptr handle_; 113 | }; 114 | 115 | ``` 116 | 117 | ### `class Lifetime` 118 | 119 | This class creates an object that can easily have its lifetime tracked by a 120 | `Lifetime_observer`. This is esentially a wrapper around a 121 | `std::shared_ptr<...>` that holds some arbitrary data that can be tracked via 122 | the `std::weak_ptr` type. 123 | 124 | This can be placed as a member inside any class to give it the ability to be 125 | tracked by a `Slot`, for instance, if the slot function will be holding a 126 | reference to some object, it'd be a good idea to track the lifetime of the 127 | object that is referenced to avoid dangling references. 128 | 129 | `sizeof(Lifetime) == 16 Bytes` 130 | 131 | ```cpp 132 | /// A class to keep track of an object's lifetime. 133 | /** A Lifetime_observer can check if a Lifetime has ended. */ 134 | class Lifetime { 135 | public: 136 | /// Create a new lifetime to track. 137 | Lifetime(); 138 | 139 | /// Create a new lifetime to track. 140 | /** Tracking does not split across multiple Lifetime objects. */ 141 | Lifetime(Lifetime const&); 142 | 143 | /// Transfers the lifetime tracking to the new instance. 144 | /** Existing trackers will now track the newly constructed lifetime. */ 145 | Lifetime(Lifetime&&) = default; 146 | 147 | /// Create a new lifetime to track, destroying the existing lifetime. 148 | /** Tracking does not split across multiple Lifetime objects. */ 149 | auto operator=(Lifetime const& rhs) -> Lifetime&; 150 | 151 | /// Transfers the lifetime of the moved from object to *this. 152 | /** Anything tracking the moved from object will now be tracking *this. */ 153 | auto operator=(Lifetime&& rhs) -> Lifetime&; 154 | 155 | public: 156 | /// Return a Lifetime_observer, to check if *this has been destroyed. 157 | /** The returned object is valid even after *this i\ destroyed. Even though 158 | * this should never throw an exception, if a memory allocation fails at 159 | * construction, it might not throw std::bad_alloc, returning nullptr. */ 160 | auto track() const -> Lifetime_observer; 161 | 162 | private: 163 | std::shared_ptr life_; 164 | }; 165 | ``` 166 | 167 | ### `class Slot` 168 | 169 | A `Slot` is a wrapper around a `std::function` object that can condition its 170 | invocation on the lifetime of other objects. If any object that is tracked by 171 | the `Slot` is destroyed, then the call operator fails by throwing an exception. 172 | The `is_expired()` and `slot_function()` methods can be used in tandem to avoid 173 | exceptions. 174 | 175 | `sizeof(Slot) == 56 Bytes` 176 | 177 | ```cpp 178 | template 179 | class Slot; 180 | 181 | /// An invocable type with object lifetime tracking. 182 | /** An object of this type always holds a valid std::function object. 183 | * The call operator will throw if any of the tracked objects are destroyed. */ 184 | template 185 | class Slot { 186 | public: 187 | using Signature_t = R(Args...); 188 | using Function_t = std::function; 189 | 190 | /// Exception thrown if any object being tracked has been destroyed. 191 | class Expired {}; 192 | 193 | public: 194 | Slot() = delete; 195 | 196 | Slot(std::nullptr_t) = delete; 197 | 198 | /// Construct a Slot with the slot function \p f and no tracked objects. 199 | /** Throws std::invalid_argument if the given function is empty. */ 200 | template 201 | Slot(F f); 202 | 203 | /// Copies the slot function and the tracked list into the new Slot. 204 | /** Tracked objects by *this are tracked by the new Slot instance. */ 205 | Slot(Slot const&) = default; 206 | 207 | /// Moves the slot function and the tracked list into the new Slot. 208 | Slot(Slot&&) = default; 209 | 210 | /// Copies the slot function and the tracked object reference container. 211 | auto operator=(Slot const&) -> Slot& = default; 212 | 213 | /// Moves the slot function and the tracked object reference container. 214 | auto operator=(Slot&&) -> Slot& = default; 215 | 216 | public: 217 | /// Track any object owned by a std::shared_ptr<...>. 218 | /** Returns *this. Tracking the same item multiple times is not checked. */ 219 | auto track(Lifetime_observer const& x) -> Slot&; 220 | 221 | /// Track Lifetime object. Convenience. 222 | /** Returns *this. Tracking the same item multiple times is not checked. */ 223 | auto track(Lifetime const& x) -> Slot&; 224 | 225 | /// Remove the observed object from the tracked objects list. 226 | /** Removes on Lifetime_observer::get_id() equality. Returns *this. Throws 227 | * std::invalid_argument if \p x is not being tracked by *this. */ 228 | auto untrack(Lifetime_observer const& x) -> Slot&; 229 | 230 | /// Remove the given Lifetime object from the tracked objects list. 231 | /** Removes on Lifetime_observer::get_id() equality. Returns *this. Throws 232 | * std::invalid_argument if \p x is not being tracked by *this.*/ 233 | auto untrack(Lifetime const& x) -> Slot&; 234 | 235 | /// Invokes the internally held function 236 | /** Throws Expired if any tracked object has been destroyed. */ 237 | template 238 | auto operator()(Arguments&&... args) const -> R; 239 | 240 | /// Check whether any of the tracked lifetimes has expired. 241 | /** Returns true if no lifetimes are being tracked. */ 242 | auto is_expired() const -> bool; 243 | 244 | /// Return a const reference to the internal std::function. 245 | /** Always returns a valid Function_t object that can be called. */ 246 | auto slot_function() const -> Function_t const&; 247 | 248 | private: 249 | Function_t f_; 250 | std::vector observers_; 251 | }; 252 | 253 | ``` 254 | 255 | ### `class Identifier` 256 | 257 | ```cpp 258 | /// Objects of this type can be unique and compared against other identifiers. 259 | class Identifier { 260 | public: 261 | using Underlying_int = std::uint32_t; 262 | 263 | public: 264 | /// Construct the initial value. 265 | Identifier(); 266 | 267 | /// Generate the next identifier value, this is an increment. 268 | static auto next(Identifier x) -> Identifier; 269 | 270 | public: 271 | /// Return true if both Identifiers have the same internal value. 272 | friend auto operator==(Identifier x, Identifier y) -> bool; 273 | 274 | /// Return true if both Identifiers do not have the same internal value. 275 | friend auto operator!=(Identifier x, Identifier y) -> bool; 276 | 277 | private: 278 | /// Used by next(...). 279 | Identifier(Underlying_int value); 280 | 281 | private: 282 | Underlying_int value_; 283 | }; 284 | ``` 285 | 286 | ### `class Signal` 287 | 288 | A `Signal` object acts as an observer, notifying registered `Slots` 289 | whenever the `Signal` is emitted. The `Signal` is able to pass on arguments to 290 | its registered `Slots`, and the return value of emitting a `Signal` is a 291 | `std::optional` containing the result of the last `Slot` called. 292 | 293 | `sizeof(Signal) == 24 Bytes` 294 | 295 | ```cpp 296 | template 297 | class Signal; 298 | 299 | /// An observer type that calls registered callbacks(Slots) when emitted. 300 | template 301 | class Signal { 302 | public: 303 | using Signature_t = R(Args...); 304 | using Emit_result_t = 305 | std::conditional_t, void, std::optional>; 306 | 307 | public: 308 | /// Construct a Signal with no connected Slots. 309 | Signal() = default; 310 | 311 | /// Create a Signal with the same Slots connected, and the same Identifiers. 312 | Signal(Signal const&) = default; 313 | 314 | /// Move the connected Slots from the existing Signal to the new one. 315 | /** The moved from Signal will be empty afterwards. */ 316 | Signal(Signal&&) = default; 317 | 318 | /// Overwrite the existing Signal with the Slots and Identifiers of the rhs. 319 | auto operator=(Signal const&) -> Signal& = default; 320 | 321 | /// Overwrite the existing Signal with the Slots and Identifiers of the rhs. 322 | /** The moved from Signal will be empty afterwards. */ 323 | auto operator=(Signal&&) -> Signal& = default; 324 | 325 | public: 326 | /// Invoke all non-expired Slots. 327 | /** Returns the return value of the last connected Slot or std::nullopt if 328 | * none. Expired Slots are ignored, rather than throwing an exception. */ 329 | auto emit(Args const&... args) const -> Emit_result_t; 330 | 331 | /// Alternative notation for Signal::emit. 332 | auto operator()(Args const&... args) const -> Emit_result_t; 333 | 334 | /// Register a Slot with *this, will be invoked when *this is emitted. 335 | /** Returns a unique Identifier, to be used with Signal::disconnect. */ 336 | auto connect(Slot s) -> Identifier; 337 | 338 | /// Removes and returns the Slot associated with the given Identifier. 339 | /** Throws std::invalid_argument if no connected Slot is found with id. */ 340 | auto disconnect(Identifier id) -> Slot; 341 | 342 | /// Return the number of connected Slots. 343 | auto slot_count() const -> std::size_t; 344 | 345 | /// Return true if there are no connected Slots. 346 | auto is_empty() const -> bool; 347 | 348 | private: 349 | std::vector>> slots_; 350 | }; 351 | ``` 352 | 353 | ## Test Code 354 | 355 | ```cpp 356 | #include 357 | 358 | // Empty Signal returns std::nullopt or void. 359 | { 360 | sl::Signal si; 361 | sl::Signal sv; 362 | assert(si() == std::nullopt); 363 | sv(); 364 | } 365 | 366 | // Single Slot connect and disconnect. 367 | { 368 | sl::Signal s; 369 | sl::Identifier id = s.connect([]{ return 5; }); 370 | assert(!s.is_empty()); 371 | assert(s.slot_count() == 1) 372 | 373 | std::optional result = s(); 374 | assert(result.is_valid() && *result == 5); 375 | s.disconnect(id); 376 | assert(s.is_empty()); 377 | assert(s.slot_count() == 0); 378 | assert(s() == std::nullopt); 379 | } 380 | 381 | // Multiple Slots connected and disconnected. 382 | { 383 | sl::Signal s; 384 | sl::Identifier sum_id = s.connect([](char c, int i, bool b) { return c + i + b; }); 385 | sl::Identifier product_id = s.connect([](char c, int i, bool b) { return c * i * b; }); 386 | sl::Identifier difference_id = s.connect([](char c, int i, bool b) { return c - i - b; }); 387 | 388 | assert(s.slot_count() == 3); 389 | assert(*s(5, 1, false) == 4); 390 | 391 | sl::Slot difference_slot = s.disconnect(difference_id); 392 | assert(difference_slot(5, 1, false) == 4); 393 | assert(s.slot_count() == 2); 394 | assert(*s(4, 3, true) == 12); 395 | 396 | sl::Slot sum_slot = s.disconnect(sum_id); 397 | assert(sum_slot(5, 5, false) == 10); 398 | assert(s.slot_count() == 1); 399 | assert(*s(5, 5, true) == 25); 400 | 401 | sl::Slot produce_slot = s.disconnect(product_id); 402 | assert(product_slot(5, 5, true) == 25); 403 | assert(s.slot_count() == 0); 404 | assert(s.is_empty()); 405 | assert(s(5, 5, true) == std::nullopt); 406 | } 407 | 408 | // void return type Slot/Signal. 409 | { 410 | sl::Signal s; 411 | assert(std::is_same_v); 412 | s.connect([](int){}); 413 | assert(std::is_same_v); 414 | } 415 | 416 | // Throwing on disconnect of invalid Identifier. 417 | { 418 | sl::Signal s; 419 | auto id = s.connect([]{}); 420 | auto slot = s.disconnect(id); 421 | expect_throw(s.disconnect(id), std::invalid_argument); 422 | expect_throw(s.disconnect(sl::Identifier{}), std::invalid_argument); 423 | } 424 | 425 | // Lifetime tracking of single object. 426 | { 427 | sl::Signal<> s; 428 | sl::Slot slot = []{ return 5; }; 429 | auto life = std::make_unique(); 430 | slot.track(*life); 431 | auto id = s.connect(slot); 432 | assert(*s() == 5); 433 | life.reset(); 434 | assert(s() == std::nullopt); 435 | } 436 | 437 | // Lifetime tracking of multiple objects. 438 | { 439 | sl::Signal s; 440 | sl::Slot slot_5 = []{ return 5; } 441 | sl::Slot slot_3 = []{ return 3; } 442 | 443 | auto life_1 = std::make_unique(); 444 | auto life_2 = std::make_unique(); 445 | auto life_3 = std::make_unique(); 446 | auto life_4 = std::make_unique(); 447 | auto life_5 = std::make_unique(); 448 | auto life_6 = std::make_unique(); 449 | 450 | slot_5.track(*life_1); 451 | slot_5.track(*life_2); 452 | slot_5.track(*life_3); 453 | slot_5.track(*life_4); 454 | slot_3.track(*life_5); 455 | slot_3.track(*life_6); 456 | 457 | s.connect(slot_5); 458 | s.connect(slot_3); 459 | 460 | assert(*s() == 3); 461 | life_1.reset(); 462 | assert(*s() == 3); 463 | life_5.reset(); 464 | assert(s() == std::nullopt); 465 | life_6.reset(); 466 | assert(s() == std::nullopt); 467 | life_2.reset(); 468 | assert(s() == std::nullopt); 469 | life_3.reset(); 470 | assert(s() == std::nullopt); 471 | life_4.reset(); 472 | assert(s() == std::nullopt); 473 | } 474 | ``` 475 | --------------------------------------------------------------------------------