├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── benchmarks ├── CMakeLists.txt └── obs_benchmarks.cpp ├── obs.h ├── obs ├── connection.cpp ├── connection.h ├── observable.h ├── observers.h ├── safe_list.h ├── signal.h └── slot.h └── tests ├── CMakeLists.txt ├── adapt_slots.cpp ├── count_signals.cpp ├── disconnect_on_dtor.cpp ├── disconnect_on_rescursive_signal.cpp ├── disconnect_on_signal.cpp ├── multithread.cpp ├── multithread_futures.cpp ├── observers.cpp ├── reconnect_on_notification.cpp ├── reconnect_on_signal.cpp ├── signals.cpp └── test.h /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | I agree that my contributions are licensed under the Observable license, and agree to future changes to the licensing. 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | os: [windows-2019, macos-latest, ubuntu-latest] 10 | build_type: [debug,release] 11 | benchmark: [false] 12 | sanitizer: [""] 13 | include: 14 | - os: macos-latest 15 | build_type: debug 16 | sanitizer: "-fsanitize=thread" 17 | - os: macos-latest 18 | build_type: debug 19 | sanitizer: "-fsanitize=address" 20 | - os: ubuntu-latest 21 | build_type: release 22 | benchmark: true 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: aseprite/get-ninja@main 26 | - uses: ilammy/msvc-dev-cmd@v1 27 | if: runner.os == 'Windows' 28 | - name: Generating Makefiles 29 | shell: bash 30 | run: | 31 | cmake . -G Ninja \ 32 | -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ 33 | -DCMAKE_CXX_FLAGS=${{ matrix.sanitizer }} \ 34 | -DCMAKE_EXE_LINKER_FLAGS=${{ matrix.sanitizer }} \ 35 | -DOBSERVABLE_BENCHMARKS=${{ matrix.benchmark }} 36 | - name: Compiling 37 | run: cmake --build . 38 | - name: Running Tests 39 | shell: bash 40 | run: | 41 | ctest --output-on-failure 42 | if [[ "${{ matrix.benchmark }}" == "true" ]] ; then ./benchmarks/obs_benchmarks ; fi 43 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Observable Library 2 | # Copyright (C) 2016-2025 David Capello 3 | 4 | cmake_minimum_required(VERSION 3.15) 5 | 6 | project(observable CXX) 7 | option(OBSERVABLE_TESTS "Compile observable tests" ON) 8 | option(OBSERVABLE_BENCHMARKS "Compile observable benchmarks" OFF) 9 | 10 | set(CMAKE_CXX_STANDARD 11) 11 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 12 | 13 | if(UNIX AND NOT APPLE) 14 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread") 15 | endif() 16 | 17 | add_library(obs obs/connection.cpp) 18 | target_include_directories(obs PUBLIC .) 19 | 20 | if(OBSERVABLE_TESTS) 21 | enable_testing() 22 | add_subdirectory(tests) 23 | endif() 24 | 25 | if(OBSERVABLE_BENCHMARKS) 26 | add_subdirectory(benchmarks) 27 | endif() 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By submitting a pull request, you represent that you have the right to 2 | license your contribution to the Observable project owners and the community, 3 | agree by submitting the patch that your contributions are licensed under 4 | the [Observable license](https://raw.githubusercontent.com/dacap/observable/main/LICENSE.txt), 5 | and agree to future changes to the licensing. 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016-2021 David Capello 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Observable Library 2 | ================== 3 | 4 | *Copyright (C) 2016-2021 David Capello* 5 | 6 | [![build](https://github.com/dacap/observable/actions/workflows/build.yml/badge.svg)](https://github.com/dacap/observable/actions/workflows/build.yml) 7 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt) 8 | 9 | Library to use the observer pattern in C++11 programs with 10 | observable/observer classes or signals/slots. 11 | 12 | Features 13 | -------- 14 | 15 | * Generate an observable notification/signal from multiple threads 16 | * Add/remove observers/slots from multiple threads 17 | * Erase/disconnect an observer/slot from the same observable notification/signal 18 | * Reconnect an observer in the same notification 19 | 20 | Observable 21 | ---------- 22 | 23 | An observable `Widget`: 24 | 25 | ```cpp 26 | #include "obs.h" 27 | 28 | class WidgetObserver { 29 | public: 30 | virtual ~WidgetObserver() = 0; 31 | virtual void onClick() { } 32 | }; 33 | 34 | class Widget : public obs::observable { 35 | public: 36 | void processClick() { 37 | notify_observers(&WidgetObserver::onClick); 38 | } 39 | }; 40 | ``` 41 | 42 | An example 43 | 44 | ```cpp 45 | #include "obs.h" 46 | 47 | class ObserveClick : public WidgetObserver { 48 | public: 49 | void onClick() override { 50 | // Do something... 51 | } 52 | }; 53 | 54 | ... 55 | ObserveClick observer; 56 | Widget button; 57 | button.add_observer(&observer); 58 | ``` 59 | 60 | Signal 61 | ------ 62 | 63 | ```cpp 64 | #include "obs.h" 65 | 66 | int main() { 67 | obs::signal sig; 68 | sig.connect([](int x, int y){ ... }); 69 | sig(1, 2); // Generate signal 70 | } 71 | ``` 72 | 73 | Tested Compilers 74 | ---------------- 75 | 76 | * Visual Studio 2015 77 | * Xcode 7.3.1 (`-std=c++11`) 78 | * GCC 4.8.4 (`-std=c++11`) 79 | -------------------------------------------------------------------------------- /benchmarks/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Observable Library 2 | # Copyright (C) 2018 David Capello 3 | 4 | include(ExternalProject) 5 | ExternalProject_Add(googlebenchmark-project 6 | URL https://github.com/google/benchmark/archive/master.zip 7 | PREFIX "${CMAKE_BINARY_DIR}/googlebenchmark" 8 | INSTALL_DIR "${CMAKE_BINARY_DIR}/googlebenchmark" 9 | BUILD_BYPRODUCTS "${CMAKE_BINARY_DIR}/googlebenchmark/lib/${CMAKE_STATIC_LIBRARY_PREFIX}benchmark${CMAKE_STATIC_LIBRARY_SUFFIX}" 10 | CMAKE_CACHE_ARGS 11 | -DBENCHMARK_ENABLE_GTEST_TESTS:BOOL=OFF 12 | -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} 13 | -DCMAKE_INSTALL_PREFIX:PATH= 14 | -DCMAKE_INSTALL_LIBDIR:PATH=/lib) 15 | 16 | ExternalProject_Get_Property(googlebenchmark-project install_dir) 17 | set(GOOGLEBENCHMARK_INCLUDE_DIRS ${install_dir}/include) 18 | set(GOOGLEBENCHMARK_LIBRARY ${install_dir}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}benchmark${CMAKE_STATIC_LIBRARY_SUFFIX}) 19 | 20 | # Create the directory so changing INTERFACE_INCLUDE_DIRECTORIES doesn't fail 21 | file(MAKE_DIRECTORY ${GOOGLEBENCHMARK_INCLUDE_DIRS}) 22 | 23 | add_library(googlebenchmark STATIC IMPORTED) 24 | set_target_properties(googlebenchmark PROPERTIES 25 | IMPORTED_LOCATION ${GOOGLEBENCHMARK_LIBRARY} 26 | INTERFACE_INCLUDE_DIRECTORIES ${GOOGLEBENCHMARK_INCLUDE_DIRS}) 27 | add_dependencies(googlebenchmark googlebenchmark-project) 28 | 29 | add_executable(obs_benchmarks obs_benchmarks.cpp) 30 | target_link_libraries(obs_benchmarks obs googlebenchmark) 31 | -------------------------------------------------------------------------------- /benchmarks/obs_benchmarks.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2018 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs.h" 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | static void BM_ObsCreation(benchmark::State& state) { 17 | for (auto _ : state) { 18 | state.PauseTiming(); 19 | { 20 | obs::signal sig; 21 | state.ResumeTiming(); 22 | } 23 | } 24 | } 25 | BENCHMARK(BM_ObsCreation); 26 | 27 | static void BM_ObsConnect(benchmark::State& state) { 28 | obs::signal sig; 29 | for (auto _ : state) 30 | sig.connect([]{ }); 31 | } 32 | BENCHMARK(BM_ObsConnect); 33 | 34 | static void BM_ObsDisconnect(benchmark::State& state) { 35 | obs::signal sig; 36 | for (auto _ : state) { 37 | state.PauseTiming(); 38 | obs::connection c = sig.connect([]{ }); 39 | state.ResumeTiming(); 40 | c.disconnect(); 41 | } 42 | } 43 | BENCHMARK(BM_ObsDisconnect); 44 | 45 | static void BM_ObsSignal(benchmark::State& state) { 46 | obs::signal sig; 47 | std::vector conns(state.range(0)); 48 | for (auto& c : conns) 49 | c = sig.connect([]{ }); 50 | for (auto _ : state) { 51 | sig(); 52 | } 53 | } 54 | BENCHMARK(BM_ObsSignal)->Range(1, 1024); 55 | 56 | static void BM_ObsThreads(benchmark::State& state) { 57 | obs::signal sig; 58 | for (auto _ : state) { 59 | state.PauseTiming(); 60 | std::vector threads; 61 | std::atomic count = { 0 }; 62 | for (int i=0; i l(m); 67 | std::condition_variable cv; 68 | obs::scoped_connection c = 69 | sig.connect( 70 | [&m, &cv]{ 71 | std::unique_lock l(m); 72 | cv.notify_one(); 73 | }); 74 | ++count; 75 | cv.wait(l); 76 | }); 77 | } 78 | 79 | // Wait that all threads are created and waiting for the signal. 80 | while (count < state.range(0)) 81 | std::this_thread::yield(); 82 | state.ResumeTiming(); 83 | 84 | sig(); 85 | 86 | state.PauseTiming(); 87 | for (auto& t : threads) 88 | t.join(); 89 | state.ResumeTiming(); 90 | } 91 | } 92 | BENCHMARK(BM_ObsThreads)->Range(1, 1024); 93 | 94 | BENCHMARK_MAIN(); 95 | -------------------------------------------------------------------------------- /obs.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef OBS_H_INCLUDED 8 | #define OBS_H_INCLUDED 9 | #pragma once 10 | 11 | #include "obs/observable.h" 12 | #include "obs/observers.h" 13 | #include "obs/signal.h" 14 | #include "obs/slot.h" 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /obs/connection.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/connection.h" 8 | #include "obs/signal.h" 9 | 10 | namespace obs { 11 | 12 | void connection::disconnect() { 13 | if (!m_slot) 14 | return; 15 | 16 | assert(m_signal); 17 | if (m_signal) 18 | m_signal->disconnect_slot(m_slot); 19 | 20 | delete m_slot; 21 | m_slot = nullptr; 22 | } 23 | 24 | } // namespace obs 25 | -------------------------------------------------------------------------------- /obs/connection.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016-2021 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef OBS_CONNETION_H_INCLUDED 8 | #define OBS_CONNETION_H_INCLUDED 9 | #pragma once 10 | 11 | namespace obs { 12 | 13 | class signal_base; 14 | class slot_base; 15 | 16 | class connection { 17 | public: 18 | connection() : m_signal(nullptr), 19 | m_slot(nullptr) { 20 | } 21 | 22 | connection(signal_base* sig, 23 | slot_base* slot) : 24 | m_signal(sig), 25 | m_slot(slot) { 26 | } 27 | 28 | void disconnect(); 29 | 30 | operator bool() { 31 | return (m_slot != nullptr); 32 | } 33 | 34 | private: 35 | signal_base* m_signal; 36 | slot_base* m_slot; 37 | }; 38 | 39 | class scoped_connection { 40 | public: 41 | scoped_connection() { 42 | } 43 | 44 | scoped_connection(const connection& conn) : m_conn(conn) { 45 | } 46 | 47 | scoped_connection& operator=(const connection& conn) { 48 | m_conn.disconnect(); 49 | m_conn = conn; 50 | return *this; 51 | } 52 | 53 | ~scoped_connection() { 54 | m_conn.disconnect(); 55 | } 56 | 57 | // Just in case that we want to disconnect the signal in the middle 58 | // of the scope. 59 | void disconnect() { 60 | m_conn.disconnect(); 61 | } 62 | 63 | private: 64 | connection m_conn; 65 | }; 66 | 67 | } // namespace obs 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /obs/observable.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef OBS_OBSERVABLE_H_INCLUDED 8 | #define OBS_OBSERVABLE_H_INCLUDED 9 | #pragma once 10 | 11 | #include "obs/observers.h" 12 | 13 | namespace obs { 14 | 15 | template 16 | class observable { 17 | public: 18 | 19 | void add_observer(Observer* observer) { 20 | m_observers.add_observer(observer); 21 | } 22 | 23 | void remove_observer(Observer* observer) { 24 | m_observers.remove_observer(observer); 25 | } 26 | 27 | void notify_observers(void (Observer::*method)()) { 28 | m_observers.notify_observers(method); 29 | } 30 | 31 | template 32 | void notify_observers(void (Observer::*method)(Args...), Args ...args) { 33 | m_observers.template notify_observers(method, std::forward(args)...); 34 | } 35 | 36 | private: 37 | observers m_observers; 38 | }; 39 | 40 | } // namespace obs 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /obs/observers.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef OBS_OBSERVERS_H_INCLUDED 8 | #define OBS_OBSERVERS_H_INCLUDED 9 | #pragma once 10 | 11 | #include "obs/safe_list.h" 12 | 13 | namespace obs { 14 | 15 | template 16 | class observers { 17 | public: 18 | typedef T observer_type; 19 | typedef safe_list list_type; 20 | typedef typename list_type::iterator iterator; 21 | 22 | bool empty() const { return m_observers.empty(); } 23 | std::size_t size() const { return m_observers.size(); } 24 | 25 | void add_observer(observer_type* observer) { 26 | m_observers.push_back(observer); 27 | } 28 | 29 | void remove_observer(observer_type* observer) { 30 | m_observers.erase(observer); 31 | } 32 | 33 | template 34 | void notify_observers(void (observer_type::*method)(Args...), Args ...args) { 35 | for (auto observer : m_observers) { 36 | if (observer) 37 | (observer->*method)(std::forward(args)...); 38 | } 39 | } 40 | 41 | private: 42 | list_type m_observers; 43 | }; 44 | 45 | } // namespace obs 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /obs/safe_list.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016-2017 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef OBS_SAFE_LIST_H_INCLUDED 8 | #define OBS_SAFE_LIST_H_INCLUDED 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace obs { 21 | 22 | // A STL-like list which is safe to remove/add items from multiple 23 | // threads while it's being iterated by multiple threads too. 24 | template 25 | class safe_list { 26 | public: 27 | class iterator; 28 | 29 | private: 30 | // A node in the linked list. 31 | struct node { 32 | // Pointer to a slot or an observer instance. 33 | // 34 | // As we cannot modify the list when we are in for-loops iterating 35 | // the list, we can temporally mark nodes as disabled changing 36 | // this "value" member to nullptr when we use erase(), Then when 37 | // the list is not iterated anymore (m_ref==0), we call 38 | // delete_nodes() to remove all disabled nodes from the list. 39 | // 40 | // We try to skip nodes with value=nullptr in the iteration 41 | // process (iterator::operator++). But it isn't possible to ensure 42 | // that an iterator will always return a non-nullptr value (so the 43 | // client have to check the return value from iterators). 44 | T* value; 45 | 46 | // Number of locks for this node, it means the number of iterators 47 | // being used and currently pointing to this node. 48 | // 49 | // This variable is incremented/decremented only when 50 | // m_mutex_nodes is locked. 51 | int locks = 0; 52 | 53 | // Next node in the list. It's nullptr for the last node in the list. 54 | node* next = nullptr; 55 | 56 | // Thread used to add the node to the list (i.e. the thread where 57 | // safe_list::push_back() was used). We suppose that the same 58 | // thread will remove the node. 59 | std::thread::id creator_thread; 60 | 61 | // Pointer to the first iterator that locked this node in the same 62 | // thread it was created. It is used to unlock() the node when 63 | // erase() is called in the same iterator loop/call. 64 | iterator* creator_thread_iterator = nullptr; 65 | 66 | node(T* value = nullptr) 67 | : value(value), 68 | creator_thread(std::this_thread::get_id()) { 69 | } 70 | 71 | node(const node&) = delete; 72 | node& operator=(const node&) = delete; 73 | 74 | // Returns true if we are using this node from the same thread 75 | // where it was created. (i.e. the thread where 76 | // safe_list::push_back() was used.) 77 | // 78 | // This function is used to know if an iterator that locks/unlocks 79 | // a node belongs to the same "creator thread," so when we erase() 80 | // the node, we can (must) unlock all those iterators. 81 | bool in_creator_thread() const { 82 | return (creator_thread == std::this_thread::get_id()); 83 | } 84 | 85 | // Locks the node by the given iterator. It means that 86 | // iterator::operator*() is going to return the node's value so we 87 | // can use it. (E.g. in case of a slot, we can call the slot 88 | // function.) 89 | void lock(iterator* it); 90 | 91 | // Indicates that the node is not being used by the given iterator 92 | // anymore. So we could delete it in case that erase() is 93 | // called. 94 | void unlock(iterator* it); 95 | 96 | // Notifies to all iterators in the "creator thread" that they 97 | // don't own a node lock anymore. It's used to erase() the node. 98 | void unlock_all(); 99 | }; 100 | 101 | // Mutex used to modify the linked-list (m_first/m_last and node::next). 102 | mutable std::mutex m_mutex_nodes; 103 | 104 | // Used to iterate the list from the first element to the last one. 105 | node* m_first = nullptr; 106 | 107 | // Used to add new items at the end of the list (with push_back()). 108 | node* m_last = nullptr; 109 | 110 | // "m_ref" indicates the number of times this list is being iterated 111 | // simultaneously. When "m_ref" reaches 0, the delete_nodes() 112 | // function is called to delete all unused nodes (unlocked nodes 113 | // with value=nullptr). While "m_ref" > 0 it means that we shouldn't 114 | // remove nodes (so we can ensure that an actual node reference is 115 | // still valid until the next unref()). 116 | std::atomic m_ref = { 0 }; 117 | 118 | // Flag that indicates if some node was erased and delete_nodes() 119 | // should iterate the whole list to clean disabled nodes (nodes with 120 | // value = nullptr). 121 | bool m_delete_nodes = false; 122 | 123 | // Used to notify when a node's locks is zero so erase() can continue. 124 | std::condition_variable m_delete_cv; 125 | 126 | public: 127 | 128 | // A STL-like iterator for safe_list. It is not a fully working 129 | // iterator, and shouldn't be used directly, it's expected to be 130 | // used only in range-based for loops. 131 | // 132 | // The iterator works in the following way: 133 | // 134 | // 1. It adds a new reference (ref()/unref()) to the safe_list so 135 | // nodes are not deleted while the iterator is alive. 136 | // 2. It "locks" the given node in iterator() ctor so the node is 137 | // not deleted when there is an existent iterator pointing to it. 138 | // 3. operator*() returns the node's value. 139 | // 4. When the iterator is incremented (operator++) it unlocks the 140 | // previous node, goes to the next one, and locks it. 141 | class iterator { 142 | public: 143 | friend struct node; 144 | 145 | typedef T* value_type; 146 | typedef std::ptrdiff_t difference_type; 147 | typedef T** pointer; 148 | typedef T*& reference; 149 | typedef std::forward_iterator_tag iterator_category; 150 | 151 | iterator(safe_list& list, node* node) 152 | : m_list(list), 153 | m_node(node) { 154 | m_list.ref(); 155 | 156 | // Lock the node because this iterator is pointing to it. 157 | if (m_node) 158 | lock(); 159 | } 160 | 161 | // Cannot copy iterators 162 | iterator(const iterator&) = delete; 163 | iterator& operator=(const iterator&) = delete; 164 | 165 | // We can only move iterators 166 | iterator(iterator&& other) 167 | : m_list(other.m_list), 168 | m_node(other.m_node) { 169 | assert(!other.m_locked); 170 | m_list.ref(); 171 | } 172 | 173 | ~iterator() { 174 | if (m_node) { 175 | std::lock_guard l(m_list.m_mutex_nodes); 176 | unlock(); 177 | } 178 | 179 | assert(!m_locked); 180 | m_list.unref(); 181 | } 182 | 183 | // Called when erase() is used from the iterators created in the 184 | // "creator thread". 185 | void notify_unlock(const node* node) { 186 | if (m_locked) { 187 | assert(m_node == node); 188 | assert(m_locked); 189 | m_locked = false; 190 | } 191 | } 192 | 193 | // Unlocks the current m_node and goes to the next one and locks it. 194 | iterator& operator++() { 195 | std::lock_guard l(m_list.m_mutex_nodes); 196 | assert(m_node); 197 | if (m_node) { 198 | unlock(); 199 | 200 | // Go to the next node. 201 | m_node = m_node->next; 202 | 203 | // Lock the new node that we're pointing to now. 204 | if (m_node) 205 | lock(); 206 | } 207 | return *this; 208 | } 209 | 210 | // Returns the node's value. The node at this point is locked. 211 | // 212 | // If the node was already deleted, it will return nullptr and the 213 | // client will need to call operator++() again. We cannot 214 | // guarantee that this function will return a value != nullptr. 215 | T* operator*() const { 216 | assert(m_node && m_locked); 217 | return m_value; 218 | } 219 | 220 | // This can be used only to compare an iterator created from 221 | // begin() (in "this" pointer) with end() ("other" argument). 222 | bool operator!=(const iterator& other) const { 223 | std::lock_guard l(m_list.m_mutex_nodes); 224 | if (m_node && other.m_node) 225 | return (m_node != other.m_node->next); 226 | else 227 | return false; 228 | } 229 | 230 | private: 231 | // Adds a lock to m_node before we access to its value. It's used 232 | // to keep track of how many iterators are using the node in the 233 | // list. 234 | void lock() { 235 | if (m_locked) 236 | return; 237 | 238 | assert(m_node); 239 | m_node->lock(this); 240 | m_value = m_node->value; 241 | m_locked = true; 242 | } 243 | 244 | void unlock() { 245 | if (!m_locked) 246 | return; 247 | 248 | assert(m_node); 249 | m_node->unlock(this); 250 | m_value = nullptr; 251 | m_locked = false; 252 | 253 | // node's locks count is zero 254 | if (m_node->locks == 0) 255 | m_list.m_delete_cv.notify_all(); 256 | } 257 | 258 | safe_list& m_list; 259 | 260 | // Current node being iterated. It is never nullptr. 261 | node* m_node; 262 | 263 | // Cached value of m_node->value 264 | T* m_value = nullptr; 265 | 266 | // True if this iterator has added a lock to the "m_node" 267 | bool m_locked = false; 268 | 269 | // Next iterator locking the same "m_node" from its creator 270 | // thread. 271 | iterator* m_next_iterator = nullptr; 272 | }; 273 | 274 | safe_list() { 275 | } 276 | 277 | ~safe_list() { 278 | assert(m_ref == 0); 279 | delete_nodes(true); 280 | 281 | assert(m_first == m_last); 282 | assert(m_first == nullptr); 283 | } 284 | 285 | bool empty() const { 286 | std::lock_guard lock(m_mutex_nodes); 287 | return (m_first == m_last); 288 | } 289 | 290 | void push_back(T* value) { 291 | node* n = new node(value); 292 | 293 | std::lock_guard lock(m_mutex_nodes); 294 | if (!m_first) 295 | m_first = m_last = n; 296 | else { 297 | m_last->next = n; 298 | m_last = n; 299 | } 300 | } 301 | 302 | void erase(T* value) { 303 | // We add a ref to avoid calling delete_nodes(). 304 | ref(); 305 | { 306 | std::unique_lock lock(m_mutex_nodes); 307 | 308 | for (node* node=m_first; node; node=node->next) { 309 | if (node->value == value) { 310 | // We disable the node so it isn't used anymore by other 311 | // iterators. 312 | assert(node->value); 313 | node->unlock_all(); 314 | node->value = nullptr; 315 | m_delete_nodes = true; 316 | 317 | // In this case we should wait until the node is unlocked, 318 | // because after erase() the client could be deleting the 319 | // value that we are using in other thread. 320 | if (node->locks) { 321 | // Wait until the node is completely unlocked by other 322 | // threads. 323 | m_delete_cv.wait(lock, [node]{ return node->locks == 0; }); 324 | } 325 | 326 | assert(node->locks == 0); 327 | 328 | // The node will be finally deleted when we leave the 329 | // iteration loop (m_ref==0, i.e. the end() iterator is 330 | // destroyed) 331 | break; 332 | } 333 | } 334 | } 335 | unref(); 336 | } 337 | 338 | iterator begin() { 339 | std::lock_guard lock(m_mutex_nodes); 340 | return iterator(*this, m_first); 341 | } 342 | 343 | iterator end() { 344 | std::lock_guard lock(m_mutex_nodes); 345 | return iterator(*this, m_last); 346 | } 347 | 348 | void ref() { 349 | #if !defined(NDEBUG) 350 | int v = 351 | #endif 352 | m_ref.fetch_add(1); 353 | assert(v >= 0); 354 | } 355 | 356 | void unref() { 357 | int v = m_ref.fetch_sub(1); 358 | assert(v >= 1); 359 | if (v == 1) 360 | delete_nodes(false); 361 | } 362 | 363 | private: 364 | // Deletes nodes from the list. If "all" is true, deletes all nodes, 365 | // if it's false, it deletes only nodes with value == nullptr, which 366 | // are nodes that were disabled 367 | void delete_nodes(bool all) { 368 | std::lock_guard lock(m_mutex_nodes); 369 | if (!all && !m_delete_nodes) 370 | return; 371 | 372 | node* prev = nullptr; 373 | node* next = nullptr; 374 | 375 | for (node* node=m_first; node; node=next) { 376 | next = node->next; 377 | 378 | if (all || (!node->value && !node->locks)) { 379 | if (prev) { 380 | prev->next = next; 381 | if (node == m_last) 382 | m_last = prev; 383 | } 384 | else { 385 | m_first = next; 386 | if (node == m_last) 387 | m_last = m_first; 388 | } 389 | 390 | assert(!node->locks); 391 | delete node; 392 | } 393 | else { 394 | prev = node; 395 | } 396 | } 397 | 398 | m_delete_nodes = false; 399 | } 400 | 401 | }; 402 | 403 | template 404 | void safe_list::node::lock(iterator* it) { 405 | ++locks; 406 | assert(locks > 0); 407 | 408 | // If we are in the creator thread, we add this iterator in the 409 | // "creator thread iterators" linked-list so the iterator is 410 | // notified in case that the node is erased. 411 | if (in_creator_thread()) { 412 | it->m_next_iterator = creator_thread_iterator; 413 | creator_thread_iterator = it; 414 | } 415 | } 416 | 417 | template 418 | void safe_list::node::unlock(iterator* it) { 419 | assert(it); 420 | 421 | // In this case we are unlocking just one iterator, if we are in the 422 | // creator thread, we've to remove this iterator from the "creator 423 | // thread iterators" linked-list. 424 | if (in_creator_thread()) { 425 | iterator* prev = nullptr; 426 | iterator* next = nullptr; 427 | for (auto it2=creator_thread_iterator; it2; it2=next) { 428 | next = it2->m_next_iterator; 429 | if (it2 == it) { 430 | if (prev) 431 | prev->m_next_iterator = next; 432 | else 433 | creator_thread_iterator = next; 434 | 435 | break; 436 | } 437 | prev = it2; 438 | } 439 | } 440 | 441 | assert(locks > 0); 442 | --locks; 443 | } 444 | 445 | // In this case we've called erase() to delete this node, so we have 446 | // to unlock the node from the creator thread if we are in the 447 | // creator thread. 448 | template 449 | void safe_list::node::unlock_all() { 450 | if (in_creator_thread()) { 451 | // Notify to all iterators in the creator thread that they don't 452 | // have the node locked anymore. In this way we can continue the 453 | // erase() call. 454 | for (auto it=creator_thread_iterator; it; it=it->m_next_iterator) { 455 | it->notify_unlock(this); 456 | 457 | assert(locks > 0); 458 | --locks; 459 | } 460 | creator_thread_iterator = nullptr; 461 | } 462 | } 463 | 464 | } // namespace obs 465 | 466 | #endif 467 | -------------------------------------------------------------------------------- /obs/signal.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef OBS_SIGNAL_H_INCLUDED 8 | #define OBS_SIGNAL_H_INCLUDED 9 | #pragma once 10 | 11 | #include "obs/connection.h" 12 | #include "obs/safe_list.h" 13 | #include "obs/slot.h" 14 | 15 | #include 16 | #include 17 | 18 | namespace obs { 19 | 20 | class signal_base { 21 | public: 22 | virtual ~signal_base() { } 23 | virtual void disconnect_slot(slot_base* slot) = 0; 24 | }; 25 | 26 | // Signal for any kind of functions 27 | template 28 | class signal { }; 29 | 30 | template 31 | class signal : public signal_base { 32 | public: 33 | typedef R result_type; 34 | typedef slot slot_type; 35 | typedef safe_list slot_list; 36 | 37 | signal() { } 38 | ~signal() { 39 | for (auto slot : m_slots) 40 | delete slot; 41 | } 42 | 43 | signal(const signal&) { } 44 | signal& operator=(const signal&) { return *this; } 45 | 46 | connection add_slot(slot_type* s) { 47 | m_slots.push_back(s); 48 | return connection(this, s); 49 | } 50 | 51 | template 52 | connection connect(Function&& f) { 53 | return add_slot(new slot_type(std::forward(f))); 54 | } 55 | 56 | template 57 | connection connect(result_type (Class::*m)(Args...args), Class* t) { 58 | return add_slot(new slot_type( 59 | [=](Args...args) -> result_type { 60 | return (t->*m)(std::forward(args)...); 61 | })); 62 | } 63 | 64 | virtual void disconnect_slot(slot_base* slot) override { 65 | m_slots.erase(static_cast(slot)); 66 | } 67 | 68 | template 69 | typename std::enable_if::value, R>::type 70 | operator()(Args2&&...args) { 71 | R result = R(); 72 | for (auto slot : m_slots) 73 | if (slot) 74 | result = (*slot)(std::forward(args)...); 75 | return result; 76 | } 77 | 78 | protected: 79 | slot_list m_slots; 80 | }; 81 | 82 | template 83 | class signal : public signal_base { 84 | public: 85 | typedef slot slot_type; 86 | typedef safe_list slot_list; 87 | 88 | signal() { } 89 | ~signal() { 90 | for (auto slot : m_slots) 91 | delete slot; 92 | } 93 | 94 | signal(const signal&) { } 95 | signal& operator=(const signal&) { return *this; } 96 | 97 | connection add_slot(slot_type* s) { 98 | m_slots.push_back(s); 99 | return connection(this, s); 100 | } 101 | 102 | template 103 | connection connect(Function&& f) { 104 | return add_slot(new slot_type(std::forward(f))); 105 | } 106 | 107 | template 108 | connection connect(void (Class::*m)(Args...args), Class* t) { 109 | return add_slot(new slot_type( 110 | [=](Args...args) { 111 | (t->*m)(std::forward(args)...); 112 | })); 113 | } 114 | 115 | virtual void disconnect_slot(slot_base* slot) override { 116 | m_slots.erase(static_cast(slot)); 117 | } 118 | 119 | template 120 | void operator()(Args2&&...args) { 121 | for (auto slot : m_slots) 122 | if (slot) 123 | (*slot)(std::forward(args)...); 124 | } 125 | 126 | protected: 127 | slot_list m_slots; 128 | }; 129 | 130 | } // namespace obs 131 | 132 | #endif 133 | -------------------------------------------------------------------------------- /obs/slot.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016-2020 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef OBS_SLOT_H_INCLUDED 8 | #define OBS_SLOT_H_INCLUDED 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | namespace obs { 15 | 16 | template 17 | struct is_callable_without_args : std::is_convertible> { }; 18 | 19 | class slot_base { 20 | public: 21 | slot_base() { } 22 | virtual ~slot_base() { } 23 | 24 | // Disable copy 25 | slot_base(const slot_base&) = delete; 26 | slot_base& operator=(const slot_base&) = delete; 27 | }; 28 | 29 | // Generic slot 30 | template 31 | class slot { }; 32 | 33 | template 34 | class slot : public slot_base { 35 | public: 36 | template::value)>::type> 39 | slot(F&& f) : f(std::forward(f)) { } 40 | 41 | template::value)>::type> 44 | slot(G g) : f([g](Args...) -> R { return g(); }) { } 45 | 46 | slot(const slot& s) { (void)s; } 47 | virtual ~slot() { } 48 | 49 | template 50 | R operator()(Args2&&...args) { 51 | assert(f); 52 | return f(std::forward(args)...); 53 | } 54 | 55 | private: 56 | std::function f; 57 | }; 58 | 59 | template 60 | class slot : public slot_base { 61 | public: 62 | template::value)>::type> 65 | slot(F&& f) : f(std::forward(f)) { } 66 | 67 | template::value)>::type> 70 | slot(G g) : f([g](Args...){ g(); }) { } 71 | 72 | slot(const slot& s) { (void)s; } 73 | virtual ~slot() { } 74 | 75 | template 76 | void operator()(Args2&&...args) { 77 | assert(f); 78 | f(std::forward(args)...); 79 | } 80 | 81 | private: 82 | std::function f; 83 | }; 84 | 85 | } // namespace obs 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Observable Library 2 | # Copyright (C) 2016-2020 David Capello 3 | 4 | function(add_observable_test name) 5 | add_executable(${name} ${name}.cpp) 6 | add_test(NAME ${name} COMMAND ${name}) 7 | target_link_libraries(${name} obs) 8 | endfunction() 9 | 10 | add_observable_test(adapt_slots) 11 | add_observable_test(count_signals) 12 | add_observable_test(disconnect_on_dtor) 13 | add_observable_test(disconnect_on_rescursive_signal) 14 | add_observable_test(disconnect_on_signal) 15 | add_observable_test(multithread) 16 | add_observable_test(multithread_futures) 17 | add_observable_test(observers) 18 | add_observable_test(reconnect_on_notification) 19 | add_observable_test(reconnect_on_signal) 20 | add_observable_test(signals) 21 | -------------------------------------------------------------------------------- /tests/adapt_slots.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2020 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | int main() { 11 | static_assert(true == obs::is_callable_without_args::value, ""); 12 | static_assert(true == obs::is_callable_without_args::value, ""); 13 | static_assert(false == obs::is_callable_without_args::value, ""); 14 | static_assert(false == obs::is_callable_without_args::value, ""); 15 | 16 | int i = 0; 17 | obs::signal a; a.connect([&i]{ ++i; }); 18 | obs::signal b; b.connect([&i]{ ++i; }); 19 | obs::signal c; c.connect([&i]{ return ++i; }); 20 | obs::signal d; d.connect([&i]{ return ++i; }); 21 | 22 | a(); 23 | b(1); 24 | c(); 25 | d(1); 26 | EXPECT_EQ(4, i); 27 | } 28 | -------------------------------------------------------------------------------- /tests/count_signals.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | #include 11 | #include 12 | 13 | int main() { 14 | std::vector threads; 15 | obs::signal sig; 16 | 17 | // We need and atomic operator++() 18 | std::atomic counter(0); 19 | 20 | for (int i=0; i<100; ++i) { 21 | sig.connect([&](){ ++counter; }); 22 | } 23 | 24 | for (int i=0; i<100; ++i) { 25 | threads.push_back( 26 | std::thread( 27 | [&](){ 28 | for (int i=0; i<100; ++i) 29 | sig(); 30 | })); 31 | } 32 | 33 | for (auto& thread : threads) 34 | thread.join(); 35 | 36 | EXPECT_EQ(100*100*100, counter); 37 | } 38 | -------------------------------------------------------------------------------- /tests/disconnect_on_dtor.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | class A { 11 | public: 12 | A(obs::signal& signal, int& value) : m_value(value) { 13 | m_conn = signal.connect(&A::on_signal, this); 14 | } 15 | 16 | private: 17 | void on_signal() { ++m_value; } 18 | 19 | obs::scoped_connection m_conn; 20 | int& m_value; 21 | }; 22 | 23 | int main() { 24 | obs::signal signal; 25 | int value = 0; 26 | { 27 | A a(signal, value); 28 | EXPECT_EQ(value, 0); 29 | signal(); 30 | EXPECT_EQ(value, 1); 31 | } 32 | signal(); 33 | EXPECT_EQ(value, 1); 34 | } 35 | -------------------------------------------------------------------------------- /tests/disconnect_on_rescursive_signal.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | 9 | int main() { 10 | obs::signal s; // We have to use "obs::" because ambiguous name from sys/signal.h on macOS? 11 | obs::connection c = s.connect( 12 | [&](int i){ 13 | if (i > 0) 14 | s(i-1); 15 | else 16 | c.disconnect(); 17 | }); 18 | s(5); 19 | } 20 | -------------------------------------------------------------------------------- /tests/disconnect_on_signal.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | class A { 11 | public: 12 | A(obs::signal& sig) { 13 | m_conn = sig.connect(&A::on_signal, this); 14 | } 15 | 16 | private: 17 | void on_signal() { 18 | m_conn.disconnect(); 19 | } 20 | 21 | obs::connection m_conn; 22 | }; 23 | 24 | int main() { 25 | obs::signal signal; 26 | { 27 | A a(signal); 28 | signal(); 29 | } 30 | signal(); 31 | } 32 | -------------------------------------------------------------------------------- /tests/multithread.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016-2018 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | #include 11 | 12 | const int N = 1; 13 | 14 | class A { 15 | int m_code; 16 | 17 | public: 18 | A(int code) : m_code(code) { 19 | EXPECT_TRUE(m_code >= 0); 20 | } 21 | 22 | ~A() { 23 | EXPECT_TRUE(m_code >= 0); 24 | m_code = -1; 25 | } 26 | 27 | void on_signal(int value) { 28 | EXPECT_TRUE(m_code >= 0); 29 | } 30 | }; 31 | 32 | int main() { 33 | obs::signal signal; 34 | std::vector threads; 35 | 36 | std::atomic count = { 0 }; 37 | signal.connect([&count](int){ ++count; }); 38 | 39 | A b(1); 40 | signal.connect(&A::on_signal, &b); 41 | 42 | for (int i=0; i<500*N; ++i) { 43 | // Emit the signal 44 | if ((i%2) == 0) { 45 | threads.push_back( 46 | std::thread( 47 | [&signal](){ 48 | for (int c=100*N; c>0; --c) { 49 | signal(c); 50 | } 51 | })); 52 | } 53 | // Observe the signal 54 | else { 55 | threads.push_back( 56 | std::thread( 57 | [&signal, i](){ 58 | A a(i); 59 | obs::scoped_connection conn = signal.connect(&A::on_signal, &a); 60 | for (int c=10*N; c>0; --c) { 61 | signal(i); 62 | } 63 | })); 64 | } 65 | 66 | signal(1000+i); 67 | } 68 | 69 | for (auto& thread : threads) 70 | thread.join(); 71 | 72 | std::cout << "Count = " << count << "\n"; 73 | } 74 | -------------------------------------------------------------------------------- /tests/multithread_futures.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2018 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | int main() { 17 | obs::signal sig; 18 | std::time_t t = std::time(nullptr); 19 | 20 | auto func = 21 | [&sig, t](){ 22 | int count = 0; 23 | while ((std::time(nullptr) - t) < 5) { 24 | std::vector conns(32); 25 | for (auto& conn : conns) { 26 | conn = sig.connect([](){ }); 27 | ++count; 28 | if (std::time(nullptr) - t >= 5) 29 | break; 30 | } 31 | } 32 | return count; 33 | }; 34 | 35 | std::vector> futures; 36 | int n = std::max(1, std::thread::hardware_concurrency()/2); 37 | while (n--) 38 | futures.emplace_back(std::async(std::launch::async, func)); 39 | 40 | int count = 0; 41 | for (auto& f : futures) 42 | count += f.get(); 43 | 44 | std::cout << "Count = " << count << "\n"; 45 | return 0; 46 | } 47 | -------------------------------------------------------------------------------- /tests/observers.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/observable.h" 8 | #include "test.h" 9 | 10 | class Observer { 11 | public: 12 | virtual ~Observer() { } 13 | virtual void on_event_a() { } 14 | virtual void on_event_b(int a) { } 15 | virtual void on_event_c(int a, int b) { } 16 | }; 17 | 18 | class O : public obs::observable { 19 | public: 20 | O() { } 21 | }; 22 | 23 | class ObserverA : public Observer { 24 | public: 25 | bool a = false; 26 | void on_event_a() override { a = true; } 27 | }; 28 | 29 | class ObserverB : public Observer { 30 | public: 31 | bool b = false; 32 | int a_arg = -1; 33 | 34 | void on_event_b(int a) override { 35 | b = true; 36 | a_arg = a; 37 | } 38 | }; 39 | 40 | class ObserverC : public Observer { 41 | public: 42 | bool c = false; 43 | int a_arg = -1; 44 | int b_arg = -1; 45 | 46 | void on_event_c(int a, int b) override { 47 | c = true; 48 | a_arg = a; 49 | b_arg = b; 50 | } 51 | }; 52 | 53 | int main() { 54 | O o; 55 | ObserverA a; 56 | ObserverB b; 57 | ObserverC c; 58 | o.add_observer(&a); 59 | o.add_observer(&b); 60 | o.add_observer(&c); 61 | 62 | o.notify_observers(&Observer::on_event_a); 63 | EXPECT_TRUE(a.a); 64 | EXPECT_FALSE(b.b); 65 | EXPECT_FALSE(c.c); 66 | 67 | o.notify_observers(&Observer::on_event_b, 1); 68 | EXPECT_TRUE(b.b); 69 | EXPECT_FALSE(c.c); 70 | EXPECT_EQ(1, b.a_arg); 71 | 72 | o.notify_observers(&Observer::on_event_c, 1, 2); 73 | EXPECT_TRUE(b.b); 74 | EXPECT_TRUE(c.c); 75 | EXPECT_EQ(1, c.a_arg); 76 | EXPECT_EQ(2, c.b_arg); 77 | } 78 | -------------------------------------------------------------------------------- /tests/reconnect_on_notification.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/observable.h" 8 | #include "test.h" 9 | 10 | class Observer { 11 | public: 12 | virtual ~Observer() { } 13 | virtual void on_event() { } 14 | }; 15 | 16 | class Observable : public obs::observable { 17 | public: 18 | Observable() { } 19 | }; 20 | 21 | Observable object; 22 | 23 | class Reconnect : public Observer { 24 | public: 25 | void on_event() override { 26 | // Here we reconnect this observer at the end of the observers 27 | // list, the library should avoid an infinite loop iterating 28 | // the same observer again and again. 29 | object.remove_observer(this); 30 | object.add_observer(this); 31 | } 32 | }; 33 | 34 | int main() { 35 | Reconnect r; 36 | object.add_observer(&r); 37 | object.notify_observers(&Observer::on_event); 38 | } 39 | -------------------------------------------------------------------------------- /tests/reconnect_on_signal.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | obs::signal sig; 11 | obs::scoped_connection conn; 12 | 13 | void func() { 14 | conn = sig.connect(func); 15 | 16 | // This second connection produced an infinite loop in an old 17 | // implementation of iterators where we skipped removed nodes in the 18 | // same operator*(). So then the iterator::operator!=() was always 19 | // true. 20 | conn = sig.connect(func); 21 | } 22 | 23 | int main() { 24 | conn = sig.connect(func); 25 | sig(); 26 | } 27 | -------------------------------------------------------------------------------- /tests/signals.cpp: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (c) 2016 David Capello 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "obs/signal.h" 8 | #include "test.h" 9 | 10 | struct Entity { 11 | int a; 12 | Entity() : a(0) { } 13 | void set_a(int v) { a = v; } 14 | int set_a_return_old(int v) { 15 | int old = a; 16 | a = v; 17 | return old; 18 | } 19 | }; 20 | 21 | int main() { 22 | { 23 | obs::signal sig; 24 | int c = 0; 25 | sig.connect([&](){ ++c; }); 26 | sig.connect([&](){ ++c; }); 27 | sig(); 28 | sig(); 29 | EXPECT_EQ(c, 4); 30 | } 31 | 32 | { 33 | obs::signal sig; 34 | int x = 2; 35 | sig.connect([&x](int y){ x += y; }); 36 | sig(3); 37 | sig(4); 38 | EXPECT_EQ(x, 9); 39 | } 40 | 41 | { 42 | obs::signal sig; 43 | int c = 0; 44 | sig.connect([&](int x, int y){ c = x+y; }); 45 | sig(3, 4); 46 | EXPECT_EQ(c, 7); 47 | } 48 | 49 | { 50 | obs::signal sig; 51 | sig.connect([](){ return 1; }); 52 | sig.connect([](){ return 4; }); 53 | int res = sig(); 54 | EXPECT_EQ(res, 4); 55 | } 56 | 57 | { 58 | int a=0, b=0; 59 | double c=0.0; 60 | obs::signal sig; 61 | sig.connect([&](int x, int y, double z) { 62 | a = x; 63 | b = y; 64 | c = z; 65 | }); 66 | sig(1, 2, 3.4); 67 | EXPECT_EQ(1, a); 68 | EXPECT_EQ(2, b); 69 | EXPECT_EQ(3.4, c); 70 | } 71 | 72 | { 73 | obs::signal sig; 74 | Entity ent; 75 | sig.connect(&Entity::set_a, &ent); 76 | EXPECT_EQ(ent.a, 0); 77 | sig(32); 78 | EXPECT_EQ(ent.a, 32); 79 | } 80 | 81 | { 82 | obs::signal sig; 83 | Entity ent; 84 | sig.connect(&Entity::set_a_return_old, &ent); 85 | ent.a = 2; 86 | EXPECT_EQ(ent.a, 2); 87 | int old = sig(32); 88 | EXPECT_EQ(old, 2); 89 | EXPECT_EQ(ent.a, 32); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/test.h: -------------------------------------------------------------------------------- 1 | // Observable Library 2 | // Copyright (C) 2016 David Capello 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | template 10 | inline void expect_eq(const T& expected, const U& value, 11 | const char* file, const int line) { 12 | if (expected != value) { 13 | std::cout << file << ":" << line << ": failed\n" 14 | << " Expected: " << expected << "\n" 15 | << " Actual: " << value << std::endl; 16 | std::abort(); 17 | } 18 | } 19 | 20 | #define EXPECT_EQ(expected, value) \ 21 | expect_eq(expected, value, __FILE__, __LINE__); 22 | 23 | #define EXPECT_TRUE(value) \ 24 | expect_eq(true, value, __FILE__, __LINE__); 25 | 26 | #define EXPECT_FALSE(value) \ 27 | expect_eq(false, value, __FILE__, __LINE__); 28 | --------------------------------------------------------------------------------