├── .clang-format ├── .github └── workflows │ ├── install.yml │ ├── macos.yml │ ├── style.yml │ ├── ubuntu.yml │ └── windows.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── CPM.cmake └── tools.cmake ├── codecov.yaml ├── include └── observe │ ├── event.h │ ├── observer.h │ └── value.h └── test ├── CMakeLists.txt └── source ├── event.cpp ├── example.cpp ├── main.cpp └── value.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | AccessModifierOffset: '-2' 4 | AlignTrailingComments: 'true' 5 | AllowAllParametersOfDeclarationOnNextLine: 'false' 6 | AlwaysBreakTemplateDeclarations: 'No' 7 | BreakBeforeBraces: Attach 8 | ColumnLimit: '100' 9 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' 10 | IncludeBlocks: Regroup 11 | IndentPPDirectives: AfterHash 12 | IndentWidth: '2' 13 | NamespaceIndentation: All 14 | BreakBeforeBinaryOperators: All 15 | BreakBeforeTernaryOperators: 'true' 16 | ... 17 | -------------------------------------------------------------------------------- /.github/workflows/install.yml: -------------------------------------------------------------------------------- 1 | name: Install 2 | 3 | on: [push] 4 | 5 | env: 6 | CTEST_OUTPUT_ON_FAILURE: 1 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: build and install library 17 | run: | 18 | CXX=g++-8 cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release 19 | sudo cmake --build build --target install 20 | rm -rf build 21 | 22 | - name: configure 23 | run: CXX=g++-8 cmake -Htest -Bbuild -DTEST_INSTALLED_VERSION=1 24 | 25 | - name: build 26 | run: cmake --build build --config Debug -j4 27 | 28 | - name: test 29 | run: | 30 | cd build 31 | ctest --build-config Debug 32 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: [push] 4 | 5 | env: 6 | CTEST_OUTPUT_ON_FAILURE: 1 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: configure 17 | run: cmake -Htest -Bbuild 18 | 19 | - name: build 20 | run: cmake --build build --config Debug -j4 21 | 22 | - name: test 23 | run: | 24 | cd build 25 | ctest --build-config Debug 26 | -------------------------------------------------------------------------------- /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: Style 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | - name: Install clang-format 14 | run: brew install clang-format 15 | 16 | - name: configure 17 | run: cmake -Htest -Bbuild 18 | 19 | - name: check style 20 | run: cmake --build build --target check-format 21 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: [push] 4 | 5 | env: 6 | CTEST_OUTPUT_ON_FAILURE: 1 7 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: configure 18 | run: CXX=g++-8 cmake -Htest -Bbuild -DENABLE_TEST_COVERAGE=1 19 | 20 | - name: build 21 | run: cmake --build build --config Debug -j4 22 | 23 | - name: test 24 | run: | 25 | cd build 26 | ctest --build-config Debug 27 | 28 | - name: install code coverage tools 29 | run: | 30 | wget https://github.com/linux-test-project/lcov/releases/download/v1.14/lcov-1.14.tar.gz 31 | tar xvfz lcov-1.14.tar.gz; 32 | sudo make install -C lcov-1.14 33 | 34 | - name: collect code coverage 35 | run: | 36 | lcov --gcov-tool $(which gcov-8) --directory . --capture --no-external --exclude "*tests*" --exclude "*_deps*" --quiet --output-file coverage.info 37 | lcov --gcov-tool $(which gcov-8) --list coverage.info 38 | bash <(curl -s https://codecov.io/bash) -f coverage.info || echo "Codecov did not collect coverage reports" 39 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: [push] 4 | 5 | env: 6 | CTEST_OUTPUT_ON_FAILURE: 1 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: configure 17 | run: cmake -Htest -Bbuild 18 | 19 | - name: build 20 | run: cmake --build build --config Debug -j4 21 | 22 | - name: test 23 | run: | 24 | cd build 25 | ctest --build-config Debug 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build* 2 | /.vscode 3 | .DS_Store -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | # ---- Project ---- 4 | 5 | # Note: update this to your new project's name and version 6 | project(Observe 7 | VERSION 3.0 8 | LANGUAGES CXX 9 | ) 10 | 11 | # ---- Include guards ---- 12 | 13 | if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) 14 | message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there.") 15 | endif() 16 | 17 | # --- Import tools ---- 18 | 19 | include(cmake/tools.cmake) 20 | 21 | # ---- Add dependencies via CPM ---- 22 | # see https://github.com/TheLartians/CPM.cmake for more info 23 | 24 | include(cmake/CPM.cmake) 25 | 26 | # PackageProject.cmake will be used to make our target installable 27 | CPMAddPackage( 28 | NAME PackageProject.cmake 29 | GITHUB_REPOSITORY TheLartians/PackageProject.cmake 30 | VERSION 1.2 31 | ) 32 | 33 | # ---- Add source files ---- 34 | FILE(GLOB_RECURSE headers CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h") 35 | 36 | # ---- Create library ---- 37 | 38 | add_library(ObserveHeaders EXCLUDE_FROM_ALL ${headers}) 39 | set_target_properties(ObserveHeaders PROPERTIES LINKER_LANGUAGE CXX) 40 | 41 | add_library(Observe INTERFACE) 42 | set_target_properties(Observe PROPERTIES INTERFACE_COMPILE_FEATURES cxx_std_17) 43 | 44 | # beeing a cross-platform target, we enforce enforce standards conformance on MSVC 45 | target_compile_options(Observe INTERFACE "$<$:/permissive->") 46 | 47 | target_include_directories(Observe 48 | INTERFACE 49 | $ 50 | $ 51 | ) 52 | 53 | # ---- Create an installable target ---- 54 | # this allows users to install and find the library via `find_package()`. 55 | 56 | packageProject( 57 | NAME ${PROJECT_NAME} 58 | VERSION ${PROJECT_VERSION} 59 | BINARY_DIR ${PROJECT_BINARY_DIR} 60 | INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include 61 | INCLUDE_DESTINATION include/${PROJECT_NAME}-${PROJECT_VERSION} 62 | DEPENDENCIES "" 63 | ) 64 | 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lars Melchior 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/TheLartians/Observe/workflows/MacOS/badge.svg)](https://github.com/TheLartians/Observe/actions) 2 | [![Actions Status](https://github.com/TheLartians/Observe/workflows/Windows/badge.svg)](https://github.com/TheLartians/Observe/actions) 3 | [![Actions Status](https://github.com/TheLartians/Observe/workflows/Ubuntu/badge.svg)](https://github.com/TheLartians/Observe/actions) 4 | [![Actions Status](https://github.com/TheLartians/Observe/workflows/Style/badge.svg)](https://github.com/TheLartians/Observe/actions) 5 | [![Actions Status](https://github.com/TheLartians/Observe/workflows/Install/badge.svg)](https://github.com/TheLartians/Observe/actions) 6 | [![codecov](https://codecov.io/gh/TheLartians/Observe/branch/master/graph/badge.svg)](https://codecov.io/gh/TheLartians/Observe) 7 | 8 | # Observe 9 | 10 | A thread-safe event-listener template and observable value implementation for C++17. 11 | 12 | ## API 13 | 14 | The core API is best illustrated by an example. 15 | 16 | ```cpp 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | void example() { 23 | // events can be valueless 24 | observe::Event<> eventA; 25 | 26 | // or have arguments 27 | observe::Event eventB; 28 | 29 | // connect will always trigger when an event is triggered 30 | eventA.connect([](){ 31 | std::cout << "A triggered" << std::endl; 32 | }); 33 | 34 | // observers will remove themselves from the event on destroy or reset 35 | observe::Observer observer = eventB.createObserver([](const std::string &str, float v){ 36 | std::cout << "B triggered with " << str << " and " << v << std::endl; 37 | }); 38 | 39 | // call emit to trigger all observers 40 | eventA.emit(); 41 | eventB.emit("meaning of life", 42); 42 | 43 | // `observe::Observer` can store any type of observer 44 | // previous observers will be removed 45 | observer.observe(eventA, [](){ std::cout << "I am now observing A" << std::endl; }); 46 | 47 | // to remove an observer without destroying the object, call reset 48 | observer.reset(); 49 | } 50 | ``` 51 | 52 | Note that events and observers are thread and exception safe, as long as the handlers manage their own resources. 53 | Handlers can safely remove observers (including themselves) from the event when beeing called. 54 | Thrown exceptions will propagate out of the `event.emit()` call. 55 | 56 | ### Using observe::Value 57 | 58 | The project also includes a header `observe/value.h` with an experimental observable value implementation. 59 | The API is still subject to change, so use with caution. 60 | 61 | ```cpp 62 | observe::Value a = 1; 63 | observe::Value b = 2; 64 | 65 | // contains the sum of `a` and `b` 66 | observe::DependentObservableValue sum([](auto a, auto b){ return a+b; },a,b); 67 | 68 | // all observable values contain an `Event` `onChange` 69 | sum.onChange.connect([](auto &v){ 70 | std::cout << "The result changed to " << r << std::endl; 71 | }); 72 | 73 | // access the value by dereferencing 74 | std::cout << "The result is " << *sum << std::endl; // -> the result is 3 75 | 76 | // changes will automatically propagate through dependent values 77 | a.set(3); // -> The result changed to 5 78 | ``` 79 | 80 | ## Installation and usage 81 | 82 | With [CPM.cmake](https://github.com/TheLartians/CPM) you can easily add the headers to your project. 83 | 84 | ```cmake 85 | CPMAddPackage( 86 | NAME Observe 87 | VERSION 3.0 88 | GITHUB_REPOSITORY TheLartians/Observe 89 | ) 90 | 91 | target_link_libraries(myProject Observe) 92 | ``` 93 | -------------------------------------------------------------------------------- /cmake/CPM.cmake: -------------------------------------------------------------------------------- 1 | # TheLartians/CPM - A simple Git dependency manager 2 | # ================================================= 3 | # See https://github.com/TheLartians/CPM for usage and update instructions. 4 | # 5 | # MIT License 6 | # ----------- 7 | #[[ 8 | Copyright (c) 2019 Lars Melchior 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | ]] 28 | 29 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 30 | 31 | set(CURRENT_CPM_VERSION 0.18) 32 | 33 | if(CPM_DIRECTORY) 34 | if(NOT ${CPM_DIRECTORY} MATCHES ${CMAKE_CURRENT_LIST_DIR}) 35 | if (${CPM_VERSION} VERSION_LESS ${CURRENT_CPM_VERSION}) 36 | message(AUTHOR_WARNING "${CPM_INDENT} \ 37 | A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ 38 | It is recommended to upgrade CPM to the most recent version. \ 39 | See https://github.com/TheLartians/CPM.cmake for more information." 40 | ) 41 | endif() 42 | return() 43 | endif() 44 | endif() 45 | 46 | option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" $ENV{CPM_USE_LOCAL_PACKAGES}) 47 | option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" $ENV{CPM_LOCAL_PACKAGES_ONLY}) 48 | option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) 49 | 50 | set(CPM_VERSION ${CURRENT_CPM_VERSION} CACHE INTERNAL "") 51 | set(CPM_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} CACHE INTERNAL "") 52 | set(CPM_PACKAGES "" CACHE INTERNAL "") 53 | set(CPM_DRY_RUN OFF CACHE INTERNAL "Don't download or configure dependencies (for testing)") 54 | 55 | if(DEFINED ENV{CPM_SOURCE_CACHE}) 56 | set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) 57 | else() 58 | set(CPM_SOURCE_CACHE_DEFAULT OFF) 59 | endif() 60 | 61 | set(CPM_SOURCE_CACHE ${CPM_SOURCE_CACHE_DEFAULT} CACHE PATH "Directory to downlaod CPM dependencies") 62 | 63 | include(FetchContent) 64 | include(CMakeParseArguments) 65 | 66 | # Initialize logging prefix 67 | if(NOT CPM_INDENT) 68 | set(CPM_INDENT "CPM:") 69 | endif() 70 | 71 | function(cpm_find_package NAME VERSION) 72 | string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") 73 | find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) 74 | if(${CPM_ARGS_NAME}_FOUND) 75 | message(STATUS "${CPM_INDENT} using local package ${CPM_ARGS_NAME}@${${CPM_ARGS_NAME}_VERSION}") 76 | CPMRegisterPackage(${CPM_ARGS_NAME} "${${CPM_ARGS_NAME}_VERSION}") 77 | set(CPM_PACKAGE_FOUND YES PARENT_SCOPE) 78 | else() 79 | set(CPM_PACKAGE_FOUND NO PARENT_SCOPE) 80 | endif() 81 | endfunction() 82 | 83 | # Find a package locally or fallback to CPMAddPackage 84 | function(CPMFindPackage) 85 | set(oneValueArgs 86 | NAME 87 | VERSION 88 | FIND_PACKAGE_ARGUMENTS 89 | ) 90 | 91 | cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) 92 | 93 | if (CPM_DOWNLOAD_ALL) 94 | CPMAddPackage(${ARGN}) 95 | cpm_export_variables() 96 | return() 97 | endif() 98 | 99 | cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) 100 | 101 | if(NOT CPM_PACKAGE_FOUND) 102 | CPMAddPackage(${ARGN}) 103 | cpm_export_variables() 104 | endif() 105 | 106 | endfunction() 107 | 108 | # Download and add a package from source 109 | function(CPMAddPackage) 110 | 111 | set(oneValueArgs 112 | NAME 113 | VERSION 114 | GIT_TAG 115 | DOWNLOAD_ONLY 116 | GITHUB_REPOSITORY 117 | GITLAB_REPOSITORY 118 | SOURCE_DIR 119 | DOWNLOAD_COMMAND 120 | FIND_PACKAGE_ARGUMENTS 121 | ) 122 | 123 | set(multiValueArgs 124 | OPTIONS 125 | ) 126 | 127 | cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") 128 | 129 | if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) 130 | cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) 131 | 132 | if(CPM_PACKAGE_FOUND) 133 | return() 134 | endif() 135 | 136 | if(CPM_LOCAL_PACKAGES_ONLY) 137 | message(SEND_ERROR "CPM: ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})") 138 | endif() 139 | endif() 140 | 141 | if (NOT DEFINED CPM_ARGS_VERSION) 142 | if (DEFINED CPM_ARGS_GIT_TAG) 143 | cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) 144 | endif() 145 | if (NOT DEFINED CPM_ARGS_VERSION) 146 | set(CPM_ARGS_VERSION 0) 147 | endif() 148 | endif() 149 | 150 | if (NOT DEFINED CPM_ARGS_GIT_TAG) 151 | set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) 152 | endif() 153 | 154 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) 155 | 156 | if(CPM_ARGS_DOWNLOAD_ONLY) 157 | set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) 158 | else() 159 | set(DOWNLOAD_ONLY NO) 160 | endif() 161 | 162 | if (CPM_ARGS_GITHUB_REPOSITORY) 163 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") 164 | endif() 165 | 166 | if (CPM_ARGS_GITLAB_REPOSITORY) 167 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") 168 | endif() 169 | 170 | if (${CPM_ARGS_NAME} IN_LIST CPM_PACKAGES) 171 | CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) 172 | if(${CPM_PACKAGE_VERSION} VERSION_LESS ${CPM_ARGS_VERSION}) 173 | message(WARNING "${CPM_INDENT} requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION}).") 174 | endif() 175 | if (CPM_ARGS_OPTIONS) 176 | foreach(OPTION ${CPM_ARGS_OPTIONS}) 177 | cpm_parse_option(${OPTION}) 178 | if(NOT "${${OPTION_KEY}}" STREQUAL ${OPTION_VALUE}) 179 | message(WARNING "${CPM_INDENT} ignoring package option for ${CPM_ARGS_NAME}: ${OPTION_KEY} = ${OPTION_VALUE} (${${OPTION_KEY}})") 180 | endif() 181 | endforeach() 182 | endif() 183 | cpm_fetch_package(${CPM_ARGS_NAME} ${DOWNLOAD_ONLY}) 184 | cpm_get_fetch_properties(${CPM_ARGS_NAME}) 185 | SET(${CPM_ARGS_NAME}_SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}") 186 | SET(${CPM_ARGS_NAME}_BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}") 187 | SET(${CPM_ARGS_NAME}_ADDED NO) 188 | cpm_export_variables() 189 | return() 190 | endif() 191 | 192 | CPMRegisterPackage(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION}) 193 | 194 | if (CPM_ARGS_OPTIONS) 195 | foreach(OPTION ${CPM_ARGS_OPTIONS}) 196 | cpm_parse_option(${OPTION}) 197 | set(${OPTION_KEY} ${OPTION_VALUE} CACHE INTERNAL "") 198 | endforeach() 199 | endif() 200 | 201 | set(FETCH_CONTENT_DECLARE_EXTRA_OPTS "") 202 | 203 | if (DEFINED CPM_ARGS_GIT_TAG) 204 | set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") 205 | else() 206 | set(PACKAGE_INFO "${CPM_ARGS_VERSION}") 207 | endif() 208 | 209 | if (DEFINED CPM_ARGS_DOWNLOAD_COMMAND) 210 | set(FETCH_CONTENT_DECLARE_EXTRA_OPTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) 211 | elseif(DEFINED CPM_ARGS_SOURCE_DIR) 212 | set(FETCH_CONTENT_DECLARE_EXTRA_OPTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) 213 | elseif (CPM_SOURCE_CACHE) 214 | string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) 215 | set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) 216 | list(SORT origin_parameters) 217 | string(SHA1 origin_hash "${origin_parameters}") 218 | set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) 219 | list(APPEND FETCH_CONTENT_DECLARE_EXTRA_OPTS SOURCE_DIR ${download_directory}) 220 | if (EXISTS ${download_directory}) 221 | list(APPEND FETCH_CONTENT_DECLARE_EXTRA_OPTS DOWNLOAD_COMMAND ":") 222 | set(PACKAGE_INFO "${download_directory}") 223 | else() 224 | # remove timestamps so CMake will re-download the dependency 225 | file(REMOVE_RECURSE ${CMAKE_BINARY_DIR}/_deps/${lower_case_name}-subbuild) 226 | set(PACKAGE_INFO "${PACKAGE_INFO} -> ${download_directory}") 227 | endif() 228 | endif() 229 | 230 | cpm_declare_fetch(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION} ${PACKAGE_INFO} "${CPM_ARGS_UNPARSED_ARGUMENTS}" ${FETCH_CONTENT_DECLARE_EXTRA_OPTS}) 231 | cpm_fetch_package(${CPM_ARGS_NAME} ${DOWNLOAD_ONLY}) 232 | cpm_get_fetch_properties(${CPM_ARGS_NAME}) 233 | SET(${CPM_ARGS_NAME}_ADDED YES) 234 | cpm_export_variables() 235 | endfunction() 236 | 237 | # export variables available to the caller to the parent scope 238 | # expects ${CPM_ARGS_NAME} to be set 239 | macro(cpm_export_variables) 240 | SET(${CPM_ARGS_NAME}_SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}" PARENT_SCOPE) 241 | SET(${CPM_ARGS_NAME}_BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" PARENT_SCOPE) 242 | SET(${CPM_ARGS_NAME}_ADDED "${${CPM_ARGS_NAME}_ADDED}" PARENT_SCOPE) 243 | endmacro() 244 | 245 | # declares that a package has been added to CPM 246 | function(CPMRegisterPackage PACKAGE VERSION) 247 | list(APPEND CPM_PACKAGES ${PACKAGE}) 248 | set(CPM_PACKAGES ${CPM_PACKAGES} CACHE INTERNAL "") 249 | set("CPM_PACKAGE_${PACKAGE}_VERSION" ${VERSION} CACHE INTERNAL "") 250 | endfunction() 251 | 252 | # retrieve the current version of the package to ${OUTPUT} 253 | function(CPMGetPackageVersion PACKAGE OUTPUT) 254 | set(${OUTPUT} "${CPM_PACKAGE_${PACKAGE}_VERSION}" PARENT_SCOPE) 255 | endfunction() 256 | 257 | # declares a package in FetchContent_Declare 258 | function (cpm_declare_fetch PACKAGE VERSION INFO) 259 | message(STATUS "${CPM_INDENT} adding package ${PACKAGE}@${VERSION} (${INFO})") 260 | 261 | if (${CPM_DRY_RUN}) 262 | message(STATUS "${CPM_INDENT} package not declared (dry run)") 263 | return() 264 | endif() 265 | 266 | FetchContent_Declare( 267 | ${PACKAGE} 268 | ${ARGN} 269 | ) 270 | endfunction() 271 | 272 | # returns properties for a package previously defined by cpm_declare_fetch 273 | function (cpm_get_fetch_properties PACKAGE) 274 | if (${CPM_DRY_RUN}) 275 | return() 276 | endif() 277 | FetchContent_GetProperties(${PACKAGE}) 278 | string(TOLOWER ${PACKAGE} lpackage) 279 | SET(${PACKAGE}_SOURCE_DIR "${${lpackage}_SOURCE_DIR}" PARENT_SCOPE) 280 | SET(${PACKAGE}_BINARY_DIR "${${lpackage}_BINARY_DIR}" PARENT_SCOPE) 281 | endfunction() 282 | 283 | # downloads a previously declared package via FetchContent 284 | function (cpm_fetch_package PACKAGE DOWNLOAD_ONLY) 285 | 286 | if (${CPM_DRY_RUN}) 287 | message(STATUS "${CPM_INDENT} package ${PACKAGE} not fetched (dry run)") 288 | return() 289 | endif() 290 | 291 | set(CPM_OLD_INDENT "${CPM_INDENT}") 292 | set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") 293 | if(${DOWNLOAD_ONLY}) 294 | if(NOT "${PACKAGE}_POPULATED") 295 | FetchContent_Populate(${PACKAGE}) 296 | endif() 297 | else() 298 | FetchContent_MakeAvailable(${PACKAGE}) 299 | endif() 300 | set(CPM_INDENT "${CPM_OLD_INDENT}") 301 | endfunction() 302 | 303 | # splits a package option 304 | function(cpm_parse_option OPTION) 305 | string(REGEX MATCH "^[^ ]+" OPTION_KEY ${OPTION}) 306 | string(LENGTH ${OPTION} OPTION_LENGTH) 307 | string(LENGTH ${OPTION_KEY} OPTION_KEY_LENGTH) 308 | if (OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) 309 | # no value for key provided, assume user wants to set option to "ON" 310 | set(OPTION_VALUE "ON") 311 | else() 312 | math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") 313 | string(SUBSTRING ${OPTION} "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) 314 | endif() 315 | set(OPTION_KEY "${OPTION_KEY}" PARENT_SCOPE) 316 | set(OPTION_VALUE "${OPTION_VALUE}" PARENT_SCOPE) 317 | endfunction() 318 | 319 | # guesses the package version from a git tag 320 | function(cpm_get_version_from_git_tag GIT_TAG RESULT) 321 | string(LENGTH ${GIT_TAG} length) 322 | if (length EQUAL 40) 323 | # GIT_TAG is probably a git hash 324 | SET(${RESULT} 0 PARENT_SCOPE) 325 | else() 326 | string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) 327 | SET(${RESULT} ${CMAKE_MATCH_1} PARENT_SCOPE) 328 | endif() 329 | endfunction() 330 | -------------------------------------------------------------------------------- /cmake/tools.cmake: -------------------------------------------------------------------------------- 1 | # this file contains a list of tools that can be activated and downloaded on-demand 2 | # each tool is enabled during configuration by passing an additional `-DUSE_=` argument to CMake 3 | 4 | # determine if a tool has already been enabled 5 | foreach(TOOL USE_SANITIZER;USE_CCACHE) 6 | get_property(${TOOL}_ENABLED GLOBAL "" PROPERTY ${TOOL}_ENABLED SET) 7 | endforeach() 8 | 9 | # enables sanitizers support using the the `USE_SANITIZER` flag 10 | # available values are: Address, Memory, MemoryWithOrigins, Undefined, Thread, Leak, 'Address;Undefined' 11 | if (USE_SANITIZER AND NOT USE_SANITIZER_ENABLED) 12 | set_property(GLOBAL PROPERTY USE_SANITIZER_ENABLED true) 13 | 14 | CPMAddPackage( 15 | NAME StableCoder-cmake-scripts 16 | GITHUB_REPOSITORY StableCoder/cmake-scripts 17 | GIT_TAG 3a469d8251660a97dbf9e0afff0a242965d40277 18 | ) 19 | 20 | include(${StableCoder-cmake-scripts_SOURCE_DIR}/sanitizers.cmake) 21 | endif() 22 | 23 | # enables CCACHE support through the USE_CCACHE flag 24 | # possible values are: YES, NO or equivalent 25 | if (USE_CCACHE AND NOT USE_CCACHE_ENABLED) 26 | set_property(GLOBAL PROPERTY USE_CCACHE_ENABLED true) 27 | 28 | CPMAddPackage( 29 | NAME Ccache.cmake 30 | GITHUB_REPOSITORY TheLartians/Ccache.cmake 31 | VERSION 1.1 32 | ) 33 | endif() 34 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "test" 3 | 4 | comment: 5 | require_changes: true -------------------------------------------------------------------------------- /include/observe/event.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace observe { 13 | 14 | template class SharedEvent; 15 | 16 | template class Event { 17 | public: 18 | /** 19 | * The handler type for this event 20 | */ 21 | using Handler = std::function; 22 | 23 | private: 24 | using HandlerID = size_t; 25 | 26 | /** 27 | * Stores an event handler 28 | */ 29 | struct StoredHandler { 30 | HandlerID id; 31 | std::shared_ptr callback; 32 | }; 33 | 34 | using HandlerList = std::vector; 35 | 36 | struct Data { 37 | HandlerID IDCounter = 0; 38 | HandlerList observers; 39 | std::mutex observerMutex; 40 | }; 41 | 42 | /** 43 | * Contains the event's data and handlers 44 | * Observers should store a `weak_ptr` to the data to observe event lifetime 45 | */ 46 | std::shared_ptr data; 47 | 48 | HandlerID addHandler(Handler h) const { 49 | std::lock_guard lock(data->observerMutex); 50 | data->observers.emplace_back(StoredHandler{data->IDCounter, std::make_shared(h)}); 51 | return data->IDCounter++; 52 | } 53 | 54 | protected: 55 | /** 56 | * Copy and assignment is `protected` to prevent accidental duplication of the event and its 57 | * handlers. If you need this, use `SharedEvent` instead. 58 | */ 59 | Event(const Event &) = default; 60 | Event &operator=(const Event &) = default; 61 | 62 | public: 63 | /** 64 | * The specific Observer implementation for this event 65 | */ 66 | class Observer : public observe::Observer::Base { 67 | private: 68 | std::weak_ptr data; 69 | HandlerID id; 70 | 71 | public: 72 | Observer() {} 73 | Observer(const std::weak_ptr &_data, HandlerID _id) : data(_data), id(_id) {} 74 | 75 | Observer(Observer &&other) = default; 76 | Observer(const Observer &other) = delete; 77 | 78 | Observer &operator=(const Observer &other) = delete; 79 | Observer &operator=(Observer &&other) = default; 80 | 81 | /** 82 | * Observe another event of the same type 83 | */ 84 | void observe(const Event &event, const Handler &handler) { 85 | reset(); 86 | *this = event.createObserver(handler); 87 | } 88 | 89 | /** 90 | * Removes the handler from the event 91 | */ 92 | void reset() { 93 | if (auto d = data.lock()) { 94 | std::lock_guard lock(d->observerMutex); 95 | auto it = std::find_if(d->observers.begin(), d->observers.end(), 96 | [&](auto &o) { return o.id == id; }); 97 | if (it != d->observers.end()) { 98 | d->observers.erase(it); 99 | } 100 | } 101 | data.reset(); 102 | } 103 | 104 | ~Observer() { reset(); } 105 | }; 106 | 107 | Event() : data(std::make_shared()) {} 108 | 109 | Event(Event &&other) : Event() { *this = std::move(other); } 110 | 111 | Event &operator=(Event &&other) { 112 | std::swap(data, other.data); 113 | return *this; 114 | } 115 | 116 | /** 117 | * Call all handlers currently connected to the event in the order they were added (thread 118 | * safe). If a handler is removed before its turn (by another thread or previous handler) it 119 | * will not be called. 120 | */ 121 | void emit(Args... args) const { 122 | std::vector> handlers; 123 | { 124 | std::lock_guard lock(data->observerMutex); 125 | handlers.resize(data->observers.size()); 126 | std::transform(data->observers.begin(), data->observers.end(), handlers.begin(), 127 | [](auto &h) { return h.callback; }); 128 | } 129 | for (auto &weakCallback : handlers) { 130 | if (auto callback = weakCallback.lock()) { 131 | (*callback)(args...); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Add a temporary handler to the event. 138 | * The handlers lifetime will be managed by the returned observer object. 139 | */ 140 | Observer createObserver(const Handler &h) const { return Observer(data, addHandler(h)); } 141 | 142 | /** 143 | * Add a permanent handler to the event. 144 | */ 145 | HandlerID connect(const Handler &h) const { return addHandler(h); } 146 | 147 | /** 148 | * Remove a permanent handler from the event. 149 | */ 150 | void disconnect(HandlerID id) const { Observer(data, id).reset(); } 151 | 152 | /** 153 | * Remove all handlers (temporary and permanent) connected to the event. 154 | */ 155 | void reset() const { 156 | std::lock_guard lock(data->observerMutex); 157 | data->observers.clear(); 158 | } 159 | 160 | /** 161 | * The number of observers connected to the event. 162 | */ 163 | size_t observerCount() const { 164 | std::lock_guard lock(data->observerMutex); 165 | return data->observers.size(); 166 | } 167 | }; 168 | 169 | /** 170 | * An event class that can be copied and assigned. 171 | * Behaves just like a more efficient `std::shared_ptr>` without derefencing. 172 | */ 173 | template class SharedEvent : public Event { 174 | public: 175 | using Event::Event; 176 | 177 | SharedEvent(const SharedEvent &other) : Event(other) {} 178 | 179 | SharedEvent &operator=(const SharedEvent &other) { 180 | Event::operator=(other); 181 | return *this; 182 | } 183 | }; 184 | 185 | } // namespace observe 186 | -------------------------------------------------------------------------------- /include/observe/observer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace observe { 6 | 7 | template class Event; 8 | 9 | /** 10 | * A generic event observer class 11 | */ 12 | class Observer { 13 | public: 14 | /** 15 | * The base class for observer implementations 16 | */ 17 | struct Base { 18 | /** 19 | * Observers should remove themselves from their events once destroyed 20 | */ 21 | virtual ~Base() {} 22 | }; 23 | 24 | Observer() {} 25 | Observer(Observer &&other) = default; 26 | template Observer(L &&l) : data(new L(std::move(l))) {} 27 | 28 | Observer &operator=(const Observer &other) = delete; 29 | Observer &operator=(Observer &&other) = default; 30 | 31 | template Observer &operator=(L &&l) { 32 | data.reset(new L(std::move(l))); 33 | return *this; 34 | } 35 | 36 | /** 37 | * Observe an event with the callback 38 | */ 39 | template void observe(Event &event, const H &handler) { 40 | data.reset(new typename Event::Observer(event.createObserver(handler))); 41 | } 42 | 43 | /** 44 | * remove the callback from the event 45 | */ 46 | void reset() { data.reset(); } 47 | 48 | /** 49 | * returns `true` if currently observing an event 50 | */ 51 | explicit operator bool() const { return bool(data); } 52 | 53 | private: 54 | std::unique_ptr data; 55 | }; 56 | 57 | } // namespace observe -------------------------------------------------------------------------------- /include/observe/value.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace observe { 10 | 11 | namespace value_detail { 12 | // source: https://stackoverflow.com/questions/6534041/how-to-check-whether-operator-exists 13 | struct No {}; 14 | template No operator==(const T &, const Arg &); 15 | template struct HasEqual { 16 | enum { value = !std::is_same::value }; 17 | }; 18 | } // namespace value_detail 19 | 20 | template class Value { 21 | protected: 22 | T value; 23 | 24 | public: 25 | using OnChange = Event; 26 | OnChange onChange; 27 | 28 | template Value(Args... args) : value(std::forward(args)...) {} 29 | 30 | Value(Value &&) = delete; 31 | Value &operator=(Value &&) = delete; 32 | 33 | template void set(Args &&... args) { 34 | if constexpr (value_detail::HasEqual::value) { 35 | T newValue(std::forward(args)...); 36 | if (value != newValue) { 37 | value = std::move(newValue); 38 | onChange.emit(value); 39 | } 40 | } else { 41 | value = T(std::forward(args)...); 42 | onChange.emit(value); 43 | } 44 | } 45 | 46 | template void setSilently(Args... args) { 47 | value = T(std::forward(args)...); 48 | } 49 | 50 | explicit operator const T &() const { return value; } 51 | 52 | const T &get() const { return value; } 53 | 54 | const T &operator*() const { return value; } 55 | 56 | const T *operator->() const { return &value; } 57 | }; 58 | 59 | template Value(T)->Value; 60 | 61 | template class DependentObservableValue : public Value { 62 | private: 63 | std::tuple::OnChange::Observer...> observers; 64 | 65 | public: 66 | template DependentObservableValue(const H &handler, const Value &... deps) 67 | : Value(handler(deps.get()...)), 68 | observers(std::make_tuple(deps.onChange.createObserver( 69 | [&, this](const auto &) { this->set(handler(deps.get()...)); })...)) {} 70 | }; 71 | 72 | template DependentObservableValue(F, const Value &...) 73 | ->DependentObservableValue::type, D...>; 74 | 75 | } // namespace observe 76 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5 FATAL_ERROR) 2 | 3 | project(ObserveTests 4 | LANGUAGES CXX 5 | ) 6 | 7 | # ---- Options ---- 8 | 9 | option(ENABLE_TEST_COVERAGE "Enable test coverage" OFF) 10 | option(TEST_INSTALLED_VERSION "Test the version found by find_package" OFF) 11 | 12 | # ---- Dependencies ---- 13 | 14 | include(../cmake/CPM.cmake) 15 | 16 | CPMAddPackage( 17 | NAME doctest 18 | GITHUB_REPOSITORY onqtam/doctest 19 | GIT_TAG 2.3.7 20 | ) 21 | 22 | if (TEST_INSTALLED_VERSION) 23 | find_package(Observe REQUIRED) 24 | else() 25 | CPMAddPackage( 26 | NAME Observe 27 | SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/.. 28 | ) 29 | endif() 30 | 31 | CPMAddPackage( 32 | NAME Format.cmake 33 | GITHUB_REPOSITORY TheLartians/Format.cmake 34 | VERSION 1.3 35 | ) 36 | 37 | # ---- Create binary ---- 38 | 39 | file(GLOB sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp) 40 | add_executable(ObserveTests ${sources}) 41 | target_link_libraries(ObserveTests doctest Observe) 42 | 43 | set_target_properties(ObserveTests PROPERTIES CXX_STANDARD 17) 44 | 45 | # enable compiler warnings 46 | if (NOT TEST_INSTALLED_VERSION) 47 | if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") 48 | target_compile_options(Observe INTERFACE -Wall -pedantic -Wextra -Werror) 49 | elseif(MSVC) 50 | target_compile_options(Observe INTERFACE /W4 /WX) 51 | endif() 52 | endif() 53 | 54 | # ---- Add ObserveTests ---- 55 | 56 | ENABLE_TESTING() 57 | 58 | include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake) 59 | doctest_discover_tests(ObserveTests) 60 | 61 | # ---- code coverage ---- 62 | 63 | if (ENABLE_TEST_COVERAGE) 64 | target_compile_options(Observe INTERFACE -O0 -g -fprofile-arcs -ftest-coverage) 65 | target_link_options(Observe INTERFACE -fprofile-arcs -ftest-coverage) 66 | endif() 67 | -------------------------------------------------------------------------------- /test/source/event.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace observe; 5 | 6 | // instantiate template for coverage 7 | template class observe::Event<>; 8 | 9 | TEST_CASE("connect and observe") { 10 | observe::Event<> event; 11 | CHECK(event.observerCount() == 0); 12 | unsigned connectCount = 0, observeCount = 0; 13 | event.connect([&]() { connectCount++; }); 14 | 15 | SUBCASE("reset observer") { 16 | observe::Event<>::Observer observer; 17 | observer = event.createObserver([&]() { observeCount++; }); 18 | for (int i = 0; i < 10; ++i) { 19 | event.emit(); 20 | } 21 | CHECK(event.observerCount() == 2); 22 | observer.reset(); 23 | CHECK(event.observerCount() == 1); 24 | for (int i = 0; i < 10; ++i) { 25 | event.emit(); 26 | } 27 | CHECK(observeCount == 10); 28 | CHECK(connectCount == 20); 29 | } 30 | 31 | SUBCASE("scoped observer") { 32 | SUBCASE("observe::Observer") { 33 | observe::Observer observer; 34 | observer.observe(event, [&]() { observeCount++; }); 35 | CHECK(event.observerCount() == 2); 36 | for (int i = 0; i < 10; ++i) { 37 | event.emit(); 38 | } 39 | } 40 | SUBCASE("observe::Event<>::Observer") { 41 | observe::Event<>::Observer observer; 42 | observer.observe(event, [&]() { observeCount++; }); 43 | CHECK(event.observerCount() == 2); 44 | for (int i = 0; i < 10; ++i) { 45 | event.emit(); 46 | } 47 | } 48 | CHECK(event.observerCount() == 1); 49 | for (int i = 0; i < 10; ++i) { 50 | event.emit(); 51 | } 52 | CHECK(observeCount == 10); 53 | CHECK(connectCount == 20); 54 | } 55 | 56 | SUBCASE("clear observers") { 57 | observe::Observer observer = event.createObserver([&]() { observeCount++; }); 58 | event.reset(); 59 | CHECK(event.observerCount() == 0); 60 | event.emit(); 61 | CHECK(connectCount == 0); 62 | } 63 | } 64 | 65 | TEST_CASE("removing observer during emit") { 66 | observe::Event<> event; 67 | observe::Event<>::Observer observer; 68 | unsigned count = 0; 69 | SUBCASE("self removing") { 70 | observer = event.createObserver([&]() { 71 | observer.reset(); 72 | count++; 73 | }); 74 | event.emit(); 75 | CHECK(count == 1); 76 | event.emit(); 77 | CHECK(count == 1); 78 | } 79 | SUBCASE("other removing") { 80 | event.connect([&]() { observer.reset(); }); 81 | observer = event.createObserver([&]() { count++; }); 82 | event.emit(); 83 | CHECK(count == 0); 84 | event.emit(); 85 | CHECK(count == 0); 86 | } 87 | } 88 | 89 | TEST_CASE("adding observers during emit") { 90 | observe::Event<> event; 91 | std::function callback; 92 | callback = [&]() { event.connect(callback); }; 93 | event.connect(callback); 94 | CHECK(event.observerCount() == 1); 95 | event.emit(); 96 | CHECK(event.observerCount() == 2); 97 | event.emit(); 98 | CHECK(event.observerCount() == 4); 99 | } 100 | 101 | TEST_CASE("emit data") { 102 | observe::Event event; 103 | int sum = 0; 104 | event.connect([&](auto a, auto b) { sum = a + b; }); 105 | event.emit(2, 3); 106 | CHECK(sum == 5); 107 | } 108 | 109 | TEST_CASE("move") { 110 | observe::Observer observer; 111 | int result = 0; 112 | observe::Event event; 113 | { 114 | observe::Event tmpEvent; 115 | observer = tmpEvent.createObserver([&](auto i) { result = i; }); 116 | tmpEvent.emit(5); 117 | CHECK(result == 5); 118 | event = Event(std::move(tmpEvent)); 119 | CHECK(tmpEvent.observerCount() == 0); 120 | } 121 | CHECK(event.observerCount() == 1); 122 | event.emit(3); 123 | CHECK(result == 3); 124 | observer.reset(); 125 | CHECK(event.observerCount() == 0); 126 | } 127 | 128 | TEST_CASE("SharedEvent") { 129 | observe::SharedEvent<> onA, onB; 130 | observe::SharedEvent<> onR(onA); 131 | unsigned aCount = 0, bCount = 0; 132 | onR.connect([&]() { aCount++; }); 133 | onA.emit(); 134 | onR = onB; 135 | onR.connect([&]() { bCount++; }); 136 | onB.emit(); 137 | CHECK(aCount == 1); 138 | CHECK(bCount == 1); 139 | } 140 | 141 | TEST_CASE("connect and disconnect") { 142 | observe::Event event; 143 | 144 | unsigned counter = 0; 145 | auto id = event.connect([&]() { counter++; }); 146 | event.emit(); 147 | CHECK(counter == 1); 148 | 149 | SUBCASE("loose id") { 150 | id = decltype(id)(); 151 | event.emit(); 152 | CHECK(counter == 2); 153 | } 154 | 155 | SUBCASE("disconnect") { 156 | event.disconnect(id); 157 | event.emit(); 158 | CHECK(counter == 1); 159 | } 160 | } -------------------------------------------------------------------------------- /test/source/example.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // clang-format off 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void example() { 11 | // events can be valueless 12 | observe::Event<> eventA; 13 | 14 | // or have arguments 15 | observe::Event eventB; 16 | 17 | // connect will always trigger when an event is triggered 18 | eventA.connect([](){ 19 | std::cout << "A triggered" << std::endl; 20 | }); 21 | 22 | // observers will remove themselves from the event on destroy or reset 23 | observe::Observer observer = eventB.createObserver([](const std::string &str, float v){ 24 | std::cout << "B triggered with " << str << " and " << v << std::endl; 25 | }); 26 | 27 | // call emit to trigger all observers 28 | eventA.emit(); 29 | eventB.emit("meaning of life", 42); 30 | 31 | // `observe::Observer` can store any type of observer 32 | observer.observe(eventA, [](){ std::cout << "I am now observing A" << std::endl; }); 33 | 34 | // to remove an observer without destroying the object, call reset 35 | observer.reset(); 36 | } 37 | 38 | // clang-format on 39 | 40 | TEST_CASE("example") { CHECK_NOTHROW(example()); } -------------------------------------------------------------------------------- /test/source/main.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | 3 | #include 4 | -------------------------------------------------------------------------------- /test/source/value.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace observe; 5 | 6 | // instantiate templates for coverage 7 | template class observe::Value; 8 | template class observe::DependentObservableValue; 9 | 10 | TEST_CASE("Value") { 11 | int current = 0; 12 | Value value(current); 13 | unsigned changes = 0; 14 | value.onChange.connect([&](auto &v) { 15 | REQUIRE(current == v); 16 | changes++; 17 | }); 18 | REQUIRE(*value == 0); 19 | REQUIRE(static_cast(value) == 0); 20 | current++; 21 | value.set(current); 22 | value.set(current); 23 | value.set(current); 24 | current++; 25 | value.set(current); 26 | value.set(current); 27 | REQUIRE(changes == 2); 28 | REQUIRE(*value == 2); 29 | } 30 | 31 | TEST_CASE("Value without comparison operator") { 32 | struct A {}; 33 | Value value; 34 | unsigned changes = 0; 35 | value.onChange.connect([&](auto &) { changes++; }); 36 | value.set(); 37 | value.set(); 38 | value.set(); 39 | REQUIRE(changes == 3); 40 | } 41 | 42 | TEST_CASE("Dependent Observable Value") { 43 | Value a(1); 44 | Value b(1); 45 | DependentObservableValue sum([](auto a, auto b) { return a + b; }, a, b); 46 | 47 | REQUIRE(*sum == 2); 48 | a.set(2); 49 | REQUIRE(*sum == 3); 50 | b.set(3); 51 | REQUIRE(*sum == 5); 52 | 53 | Value c(3); 54 | DependentObservableValue prod([](auto a, auto b) { return a * b; }, sum, c); 55 | 56 | REQUIRE(*prod == 15); 57 | a.set(1); 58 | REQUIRE(*prod == 12); 59 | b.set(4); 60 | REQUIRE(*prod == 15); 61 | c.set(2); 62 | REQUIRE(*prod == 10); 63 | 64 | c.setSilently(3); 65 | REQUIRE(*prod == 10); 66 | } 67 | 68 | TEST_CASE("Operators") { 69 | using namespace observe; 70 | struct A { 71 | int a = 0; 72 | }; 73 | Value value; 74 | REQUIRE(value->a == 0); 75 | } --------------------------------------------------------------------------------