├── vendor ├── CMakeLists.txt └── catch │ ├── CMakeLists.txt │ └── include │ └── catch │ └── catch.hpp ├── examples ├── CMakeLists.txt └── capture_the_dot │ ├── screenshot.png │ ├── src │ ├── main.cpp │ ├── game_controller.h │ ├── game_view.cpp │ ├── game_view.h │ ├── game_model.h │ ├── game_controller.cpp │ └── game_model.cpp │ ├── CMakeLists.txt │ └── README.rst ├── tests ├── src │ ├── main.cpp │ ├── detail │ │ ├── type_traits.cpp │ │ └── collection.cpp │ ├── infinite_subscription.cpp │ ├── shared_subscription.cpp │ ├── unique_subscription.cpp │ ├── expressions │ │ ├── operators.cpp │ │ ├── expression.cpp │ │ ├── math.cpp │ │ ├── filters.cpp │ │ └── tree.cpp │ ├── observe.cpp │ └── subject.cpp └── CMakeLists.txt ├── docs ├── example_tests │ ├── src │ │ ├── main.cpp │ │ ├── index.cpp │ │ ├── utility.h │ │ ├── readme.cpp │ │ ├── using_expressions.cpp │ │ └── getting_started.cpp │ └── CMakeLists.txt ├── sphinx │ ├── pages │ │ ├── index.rst │ │ └── using_expressions.rst │ ├── conf.py │ ├── CMakeLists.txt │ ├── developers.rst │ ├── index.rst │ └── getting-started.rst ├── CMakeLists.txt └── doxygen │ ├── CMakeLists.txt │ ├── style.css │ ├── Doxyfile │ └── DoxygenLayout.xml ├── .gitignore ├── visual-studio.runsettings ├── cmake ├── FindSphinx.cmake ├── default_source_groups.cmake ├── setup_qt.cmake └── compile_flags.cmake ├── benchmark ├── src │ ├── qt.h │ ├── function.cpp │ ├── virtual.cpp │ ├── qt.cpp │ ├── expressions.cpp │ └── utility.h └── CMakeLists.txt ├── CMakeLists.txt ├── observable ├── include │ └── observable │ │ ├── observable.hpp │ │ ├── detail │ │ ├── compiler_config.hpp │ │ ├── type_traits.hpp │ │ └── collection.hpp │ │ ├── expressions │ │ ├── utility.hpp │ │ ├── operators.hpp │ │ ├── expression.hpp │ │ ├── tree.hpp │ │ └── filters.hpp │ │ ├── observe.hpp │ │ ├── subscription.hpp │ │ └── subject.hpp └── CMakeLists.txt ├── .travis.yml └── README.rst /vendor/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(catch) 2 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(capture_the_dot) 2 | -------------------------------------------------------------------------------- /tests/src/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch/catch.hpp" 3 | -------------------------------------------------------------------------------- /docs/example_tests/src/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include 3 | -------------------------------------------------------------------------------- /docs/sphinx/pages/index.rst: -------------------------------------------------------------------------------- 1 | Articles 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :glob: 7 | 8 | * -------------------------------------------------------------------------------- /examples/capture_the_dot/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddinu/observable/HEAD/examples/capture_the_dot/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | .vscode/ 3 | 4 | # CLion/IDEA 5 | .idea/ 6 | 7 | # The default out of source build folder 8 | build/ 9 | .vs/ 10 | -------------------------------------------------------------------------------- /vendor/catch/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(catch INTERFACE) 2 | 3 | target_include_directories(catch 4 | INTERFACE 5 | $ 6 | ) 7 | 8 | -------------------------------------------------------------------------------- /vendor/catch/include/catch/catch.hpp: -------------------------------------------------------------------------------- 1 | #if defined(_MSC_VER) 2 | #pragma warning(push, 1) 3 | #endif 4 | 5 | #include 6 | 7 | #if defined(_MSC_VER) 8 | #pragma warning(pop) 9 | #endif 10 | -------------------------------------------------------------------------------- /visual-studio.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests$ 6 | 7 | -------------------------------------------------------------------------------- /docs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(doxygen) 2 | add_subdirectory(sphinx) 3 | add_subdirectory(example_tests) 4 | 5 | add_custom_target(ALL_DOCS) 6 | 7 | if(TARGET doxygen_docs) 8 | add_dependencies(ALL_DOCS doxygen_docs) 9 | endif() 10 | 11 | if(TARGET sphinx_docs) 12 | add_dependencies(ALL_DOCS sphinx_docs) 13 | endif() -------------------------------------------------------------------------------- /cmake/FindSphinx.cmake: -------------------------------------------------------------------------------- 1 | include(FindPackageHandleStandardArgs) 2 | 3 | find_program(SPHINX_EXECUTABLE 4 | NAMES sphinx-build 5 | HINTS 6 | PATH_SUFFIXES bin 7 | DOC "Sphinx") 8 | 9 | find_package_handle_standard_args(Sphinx DEFAULT_MSG SPHINX_EXECUTABLE) 10 | 11 | mark_as_advanced(SPHINX_EXECUTABLE) -------------------------------------------------------------------------------- /benchmark/src/qt.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "utility.h" 3 | 4 | class Sender : public QObject 5 | { 6 | Q_OBJECT 7 | 8 | signals: 9 | NOINLINE void inc(int); 10 | }; 11 | 12 | class Receiver : public QObject 13 | { 14 | Q_OBJECT 15 | 16 | public slots: 17 | NOINLINE void inc(int v) { dummy += v; } 18 | 19 | public: 20 | volatile unsigned long long dummy { 0 }; 21 | }; 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(observable) 2 | cmake_minimum_required(VERSION 3.5) 3 | enable_testing() 4 | 5 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 6 | set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "cmake") 7 | 8 | list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) 9 | 10 | add_subdirectory(observable) 11 | add_subdirectory(tests) 12 | add_subdirectory(benchmark) 13 | add_subdirectory(docs) 14 | add_subdirectory(examples) 15 | add_subdirectory(vendor) 16 | -------------------------------------------------------------------------------- /docs/sphinx/conf.py: -------------------------------------------------------------------------------- 1 | project = "Observable" 2 | master_doc = 'index' 3 | 4 | html_theme = 'alabaster' 5 | html_theme_options = { 6 | 'description': "Generic observable objects for C++", 7 | 'github_user': 'ddinu', 8 | 'github_repo': 'observable', 9 | 'github_button': True, 10 | 'font_family': 'Helvetica, Arial, sans-serif', 11 | 'head_font_family': 'Helvetica, Arial, sans-serif', 12 | } 13 | pygments_style = 'xcode' 14 | 15 | html_sidebars = { '**': ['globaltoc.html', 'searchbox.html'] } 16 | -------------------------------------------------------------------------------- /cmake/default_source_groups.cmake: -------------------------------------------------------------------------------- 1 | source_group(include REGULAR_EXPRESSION .*/include/.*) 2 | source_group(include\\detail REGULAR_EXPRESSION .*/include/.*/detail/.*) 3 | source_group(include\\expressions REGULAR_EXPRESSION .*/include/.*/expressions/.*) 4 | 5 | source_group(src REGULAR_EXPRESSION .*\\.cpp) 6 | source_group(src REGULAR_EXPRESSION .*\\.cc) 7 | source_group(src REGULAR_EXPRESSION .*\\.h) 8 | source_group(src REGULAR_EXPRESSION .*/src/.*) 9 | source_group(src\\detail REGULAR_EXPRESSION .*/src/detail/.*) 10 | source_group(src\\expressions REGULAR_EXPRESSION .*/src/expressions/.*) 11 | -------------------------------------------------------------------------------- /docs/example_tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(compile_flags) 2 | include(default_source_groups) 3 | 4 | add_executable(docs_example_tests 5 | src/main.cpp 6 | src/utility.h 7 | src/getting_started.cpp 8 | src/index.cpp 9 | src/readme.cpp 10 | src/using_expressions.cpp 11 | ) 12 | 13 | set_target_properties(docs_example_tests PROPERTIES FOLDER docs) 14 | configure_compiler(docs_example_tests) 15 | target_include_directories(docs_example_tests PRIVATE src) 16 | target_link_libraries(docs_example_tests observable catch) 17 | 18 | add_test(NAME docs_example_tests 19 | COMMAND docs_example_tests 20 | WORKING_DIRECTORY $) 21 | -------------------------------------------------------------------------------- /cmake/setup_qt.cmake: -------------------------------------------------------------------------------- 1 | # Prepare everything for using FindQt5 and building a Qt5 target. 2 | macro(setup_qt) 3 | if(NOT QT_ROOT) 4 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 5 | # Search for 64bit Qt 6 | set(QT_ROOT $ENV{QT_ROOT_64}) 7 | else() 8 | # Search for 32bit Qt 9 | set(QT_ROOT $ENV{QT_ROOT_32}) 10 | endif() 11 | endif() 12 | 13 | list(APPEND CMAKE_PREFIX_PATH ${QT_ROOT}/lib/cmake/Qt5) 14 | set(CMAKE_AUTOMOC ON) 15 | set(CMAKE_AUTORCC ON) 16 | set(CMAKE_AUTOUIC ON) 17 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 18 | set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER moc) 19 | source_group(moc REGULAR_EXPRESSION .*/moc_.+\\.cpp) 20 | endmacro() 21 | -------------------------------------------------------------------------------- /observable/include/observable/observable.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // All the useful headers. 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Some Doxygen boilerplate. 11 | 12 | //! Top-level observable classes. 13 | //! \defgroup observable Observable 14 | 15 | //! Everything related to observable expressions. 16 | //! \defgroup observable_expressions Expressions 17 | //! \ingroup observable 18 | 19 | //! These classes are internal to the library, they may change at any time. You 20 | //! should not use these classes in your code. 21 | //! \defgroup observable_detail Private 22 | //! \ingroup observable 23 | -------------------------------------------------------------------------------- /examples/capture_the_dot/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "game_controller.h" 5 | #include "game_model.h" 6 | #include "game_view.h" 7 | 8 | int main(int argc, char ** argv) 9 | { 10 | QApplication app { argc, argv }; 11 | 12 | // Create the model, view and controller and setup the game inside a window. 13 | 14 | auto window = std::make_shared(); 15 | 16 | auto view = std::shared_ptr { window, new GameView { } }; 17 | window->setCentralWidget(view.get()); 18 | 19 | auto model = std::make_shared(); 20 | window->resize(model->scene_width.get(), model->scene_height.get()); 21 | 22 | GameController game { view, model }; 23 | game.start(); 24 | 25 | window->show(); 26 | return app.exec(); 27 | } 28 | -------------------------------------------------------------------------------- /docs/example_tests/src/index.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utility.h" 6 | 7 | using namespace std::string_literals; 8 | 9 | TEST_CASE("documentation examples/index", "[documentation examples]") 10 | { 11 | capture_cout cout_buf { }; 12 | 13 | // BEGIN EXAMPLE CODE 14 | using namespace std; 15 | using namespace observable; 16 | 17 | //int main() 18 | { 19 | auto a = value { 5 }; 20 | auto b = value { 7 }; 21 | auto avg = observe((a + b) / 2.0f); 22 | 23 | avg.subscribe([](auto const & val) { cout << val; }); 24 | 25 | b = 15; 26 | // 10 will be printed on stdout. 27 | 28 | //return 0; 29 | } 30 | // END EXAMPLE CODE 31 | 32 | REQUIRE("10"s == cout_buf.str()); 33 | } 34 | -------------------------------------------------------------------------------- /docs/doxygen/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOURCES Doxyfile 2 | DoxygenLayout.xml) 3 | 4 | set(OUTPUT_DIR ${CMAKE_BINARY_DIR}/docs/html/reference) 5 | 6 | set(DOXYGEN_SKIP_DOT on) 7 | find_package(Doxygen) 8 | 9 | if(DOXYGEN_FOUND) 10 | configure_file(Doxyfile Doxyfile) 11 | 12 | add_custom_target(doxygen_docs 13 | COMMAND ${CMAKE_COMMAND} -E remove_directory "${OUTPUT_DIR}" 14 | COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_DIR}" 15 | COMMAND ${DOXYGEN_EXECUTABLE} Doxyfile 16 | SOURCES ${SOURCES} 17 | COMMENT "Building the Doxygen documentation." 18 | BYPRODUCTS ${OUTPUT_DIR}) 19 | set_target_properties(doxygen_docs PROPERTIES FOLDER docs) 20 | else() 21 | message(STATUS "Doxygen not found. Will not create the doxygen_docs target.") 22 | endif() 23 | -------------------------------------------------------------------------------- /docs/sphinx/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOURCES conf.py 2 | developers.rst 3 | getting-started.rst 4 | index.rst) 5 | 6 | set(OUTPUT_DIR ${CMAKE_BINARY_DIR}/docs/html) 7 | 8 | find_package(PythonInterp) 9 | find_package(Sphinx) 10 | 11 | if(PYTHONINTERP_FOUND AND SPHINX_FOUND) 12 | add_custom_target(sphinx_docs 13 | COMMAND ${SPHINX_EXECUTABLE} -b html ${CMAKE_CURRENT_SOURCE_DIR} ${OUTPUT_DIR} 14 | SOURCES ${SOURCES} 15 | COMMENT "Building the Sphinx documentation." 16 | BYPRODUCTS ${OUTPUT_DIR}) 17 | set_target_properties(sphinx_docs PROPERTIES FOLDER docs) 18 | else() 19 | if(NOT PYTHONINTERP_FOUND) 20 | message(STATUS "Python not found. Will not create the sphinx_docs documentation target.") 21 | endif() 22 | 23 | if(NOT SPHINX_FOUND) 24 | message(STATUS "Sphinx not found. Will not create the sphinx_docs documentation target.") 25 | endif() 26 | endif() -------------------------------------------------------------------------------- /benchmark/src/function.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utility.h" 6 | 7 | static auto const repeat_count = 1'000'000; 8 | static volatile auto dummy = 0ull; 9 | NOINLINE void function(int v) { dummy += v; } 10 | 11 | void bench() 12 | { 13 | auto function_duration = benchmark::time_run([]() { function(1); }, repeat_count); 14 | 15 | assert(dummy == repeat_count); 16 | dummy = 0; 17 | 18 | auto subject = observable::subject { }; 19 | subject.subscribe(function).release(); 20 | 21 | auto subject_duration = benchmark::time_run([&]() { subject.notify(1); }, 22 | repeat_count); 23 | 24 | assert(dummy == repeat_count); 25 | dummy = 0; 26 | 27 | benchmark::print("Function", function_duration, "Subject", subject_duration); 28 | } 29 | 30 | int main() 31 | { 32 | for(auto i = 0; i < 5; ++i) 33 | { 34 | bench(); 35 | std::cout << std::endl; 36 | } 37 | 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(compile_flags) 2 | include(default_source_groups) 3 | 4 | add_executable(tests 5 | src/main.cpp 6 | src/detail/collection.cpp 7 | src/detail/type_traits.cpp 8 | src/expressions/expression.cpp 9 | src/expressions/filters.cpp 10 | src/expressions/math.cpp 11 | src/expressions/operators.cpp 12 | src/expressions/tree.cpp 13 | src/infinite_subscription.cpp 14 | src/observe.cpp 15 | src/shared_subscription.cpp 16 | src/subject.cpp 17 | src/unique_subscription.cpp 18 | src/value.cpp 19 | ) 20 | 21 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) 22 | set_source_files_properties(src/expressions/operators.cpp PROPERTIES 23 | COMPILE_FLAGS /bigobj) 24 | endif() 25 | 26 | configure_compiler(tests) 27 | target_link_libraries(tests observable catch) 28 | target_include_directories(tests PRIVATE src) 29 | 30 | find_package(Threads) 31 | if(THREADS_FOUND) 32 | target_link_libraries(tests Threads::Threads) 33 | endif() 34 | 35 | add_test(NAME tests 36 | COMMAND tests 37 | WORKING_DIRECTORY $) 38 | -------------------------------------------------------------------------------- /examples/capture_the_dot/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(compile_flags) 2 | include(default_source_groups) 3 | include(setup_qt) 4 | 5 | setup_qt() 6 | find_package(Qt5 QUIET OPTIONAL_COMPONENTS Core Gui Widgets) 7 | 8 | if(Qt5_FOUND) 9 | message(STATUS "Creating 'capture_the_dot' target. Using Qt5 from: ${QT_ROOT}") 10 | 11 | add_executable(capture_the_dot WIN32 12 | src/game_controller.h 13 | src/game_controller.cpp 14 | src/game_model.h 15 | src/game_model.cpp 16 | src/game_view.h 17 | src/game_view.cpp 18 | src/main.cpp 19 | README.rst 20 | ) 21 | 22 | set_cpp_standard(capture_the_dot) 23 | set_target_properties(capture_the_dot PROPERTIES FOLDER examples) 24 | target_link_libraries(capture_the_dot observable 25 | Qt5::Core 26 | Qt5::Gui 27 | Qt5::Widgets) 28 | 29 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) 30 | set_target_properties(capture_the_dot PROPERTIES LINK_FLAGS /ENTRY:mainCRTStartup) 31 | endif() 32 | endif() 33 | -------------------------------------------------------------------------------- /docs/doxygen/style.css: -------------------------------------------------------------------------------- 1 | body, table, div, p, dl { 2 | font-family: Helvetica, Arial, sans-serif !important; 3 | } 4 | 5 | #titlearea { 6 | padding: 1rem; 7 | border: none; 8 | } 9 | 10 | #projectname { 11 | font-size: 2rem; 12 | } 13 | 14 | .sm, .sm a, div.header { 15 | background-image: none; 16 | border: none; 17 | background-color: inherit; 18 | } 19 | 20 | #main-nav { 21 | background-color: #F7F7F7; 22 | } 23 | 24 | .mdescLeft, .mdescRight, .memItemLeft, .memItemRight, .memTemplItemLeft, 25 | .memTemplItemRight, .memTemplParams { 26 | background-color: inherit; 27 | } 28 | 29 | .memtitle { 30 | background-color: inherit; 31 | background-image: none; 32 | border-color: #DEE4F0 !important; 33 | } 34 | 35 | .memdoc { 36 | border-color: #DEE4F0 !important; 37 | background-image: none; 38 | box-shadow: none; 39 | } 40 | 41 | .memproto { 42 | border-bottom: 1px solid; 43 | border-color: #DEE4F0 !important; 44 | background-color: inherit; 45 | background-image: none; 46 | box-shadow: none; 47 | } 48 | 49 | .groupheader { 50 | background-color: #F7F7F7; 51 | border-bottom-color: #DEE4F0 !important; 52 | padding: 0.3rem; 53 | } 54 | 55 | .navpath ul { 56 | background-image: none; 57 | } -------------------------------------------------------------------------------- /examples/capture_the_dot/src/game_controller.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | class GameView; 8 | class GameModel; 9 | 10 | //! Link a game view to a game model and manage the game play timer. 11 | class GameController 12 | { 13 | public: 14 | //! Create a new game controller. 15 | //! 16 | //! \param view The game view to use and link to the model. 17 | //! \param model The game model to use and link to the view. 18 | GameController(std::shared_ptr view, std::shared_ptr model); 19 | 20 | //! Start the game. 21 | void start(); 22 | 23 | //! Stop the game. 24 | //! 25 | //! \note This only stops the game timer. 26 | void stop(); 27 | 28 | //! Destructor. 29 | //! 30 | //! The destructor will unlink the view and the model, unsubscribing all 31 | //! observers and disconnecting all view signals. 32 | ~GameController(); 33 | 34 | private: 35 | std::shared_ptr view_; 36 | std::shared_ptr model_; 37 | std::vector subs_; 38 | std::vector cons_; 39 | 40 | std::unique_ptr timer_ { std::make_unique() }; 41 | }; 42 | -------------------------------------------------------------------------------- /observable/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(observable INTERFACE) 2 | 3 | add_custom_target(observable_headers # Just to generate a project in IDEs. 4 | SOURCES 5 | include/observable/observable.hpp 6 | include/observable/observe.hpp 7 | include/observable/subject.hpp 8 | include/observable/subscription.hpp 9 | include/observable/value.hpp 10 | include/observable/expressions/expression.hpp 11 | include/observable/expressions/filters.hpp 12 | include/observable/expressions/math.hpp 13 | include/observable/expressions/operators.hpp 14 | include/observable/expressions/tree.hpp 15 | include/observable/expressions/utility.hpp 16 | include/observable/detail/collection.hpp 17 | include/observable/detail/compiler_config.hpp 18 | include/observable/detail/type_traits.hpp 19 | ) 20 | 21 | target_include_directories(observable 22 | INTERFACE 23 | $ 24 | $ 25 | ) 26 | 27 | install(TARGETS observable EXPORT Observable 28 | INCLUDES DESTINATION include 29 | ) 30 | 31 | install(EXPORT Observable NAMESPACE Observable:: 32 | FILE ObservableTargets.cmake 33 | DESTINATION lib/cmake/observable) 34 | 35 | -------------------------------------------------------------------------------- /benchmark/src/virtual.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utility.h" 6 | 7 | static auto const repeat_count = 1'000'000; 8 | static volatile auto dummy = 0ull; 9 | NOINLINE void function(int v) { dummy += v; } 10 | 11 | struct Base 12 | { 13 | virtual void function(int) =0; 14 | }; 15 | 16 | struct Derived : Base 17 | { 18 | virtual void function(int v) { ::function(v); } 19 | }; 20 | 21 | void bench() 22 | { 23 | auto base = std::unique_ptr { std::make_unique() }; 24 | 25 | auto virtual_duration = benchmark::time_run([&]() { base->function(1); }, repeat_count); 26 | 27 | assert(dummy == repeat_count); 28 | dummy = 0; 29 | 30 | auto subject = observable::subject { }; 31 | subject.subscribe(function).release(); 32 | 33 | auto subject_duration = benchmark::time_run([&]() { subject.notify(1); }, 34 | repeat_count); 35 | 36 | assert(dummy == repeat_count); 37 | dummy = 0; 38 | 39 | benchmark::print("Virtual function", virtual_duration, "Subject", subject_duration); 40 | } 41 | 42 | int main() 43 | { 44 | for(auto i = 0; i < 5; ++i) 45 | { 46 | bench(); 47 | std::cout << std::endl; 48 | } 49 | 50 | return 0; 51 | } 52 | -------------------------------------------------------------------------------- /docs/example_tests/src/utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | //! Capture all output from std::cout for as long as the class is alive. 7 | struct capture_cout 8 | { 9 | //! Constructor. Will start capturing std::cout. 10 | capture_cout() 11 | { 12 | std::cout.rdbuf(stream_.rdbuf()); 13 | } 14 | 15 | //! Destructor. Will stop capturing std::cout. 16 | ~capture_cout() 17 | { 18 | std::cout.rdbuf(cout_buf_); 19 | } 20 | 21 | //! Return whatever was captured from the time this class was constructed, 22 | //! until now. 23 | auto str() const { return stream_.str(); } 24 | 25 | private: 26 | std::stringstream stream_; 27 | std::streambuf * cout_buf_ { std::cout.rdbuf() }; 28 | }; 29 | 30 | //! Provide input to std::cin for as long as the class is alive. 31 | struct provide_cin 32 | { 33 | //! Constructor. Will provide the input string to std::cin. 34 | provide_cin(std::string const & input) 35 | { 36 | stream_.str(input); 37 | std::cin.rdbuf(stream_.rdbuf()); 38 | } 39 | 40 | //! Destructor. Will stop providing input to std::cin. 41 | ~provide_cin() 42 | { 43 | std::cin.rdbuf(cin_buf_); 44 | } 45 | 46 | private: 47 | std::stringstream stream_; 48 | std::streambuf * cin_buf_ { std::cin.rdbuf() }; 49 | }; 50 | -------------------------------------------------------------------------------- /docs/sphinx/developers.rst: -------------------------------------------------------------------------------- 1 | Developers 2 | ========== 3 | 4 | You are welcome to contribute code, bug reports, feature requests or just ask 5 | questions. 6 | 7 | The code is hosted on GitHub: https://github.com/ddinu/observable 8 | 9 | Coding standard 10 | --------------- 11 | 12 | Please try to follow the `C++ Core Guidelines 13 | `_ as much as 14 | possible. 15 | 16 | All code should be as generic as possible. 17 | 18 | There is no big, formal coding standard, but you should follow the guidelines 19 | below: 20 | 21 | - Class names must be lowercase and names to be separated_by_underscores. 22 | - Everything must be documented. 23 | - Everything must be tested. 24 | - Try to match the code style in the file that you're editing. 25 | - Keep files under a couple hundred lines. Definitely keep them under 500 26 | lines (comments, includes and all). 27 | - Limit yourself to 80 characters per line. 28 | 29 | Testing 30 | ------- 31 | 32 | Everything should be tested, including code from the examples. Before any 33 | commit, please run the tests. Test failures **will** break the build. 34 | 35 | Please think about testing before designing the code. 36 | 37 | The testing framework used is Google Test. 38 | 39 | All tests should be runnable by just starting the test binary and by `make tests` 40 | (CMake easily enables this). 41 | -------------------------------------------------------------------------------- /docs/doxygen/Doxyfile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = "Observable Reference" 2 | PROJECT_BRIEF = "Generic observable objects for C++" 3 | OUTPUT_DIRECTORY = ${OUTPUT_DIR} 4 | INPUT = ${CMAKE_SOURCE_DIR}/observable/include/observable 5 | LAYOUT_FILE = ${CMAKE_CURRENT_SOURCE_DIR}/DoxygenLayout.xml 6 | 7 | JAVADOC_AUTOBRIEF = YES 8 | OPTIMIZE_OUTPUT_FOR_C = NO 9 | OPTIMIZE_OUTPUT_JAVA = NO 10 | BUILTIN_STL_SUPPORT = YES 11 | EXTRACT_PRIVATE = NO 12 | EXTRACT_ALL = YES 13 | HIDE_UNDOC_MEMBERS = YES 14 | HIDE_UNDOC_CLASSES = YES 15 | SHOW_INCLUDE_FILES = NO 16 | SHOW_USED_FILES = NO 17 | SORT_MEMBER_DOCS = NO 18 | QUIET = YES 19 | WARN_NO_PARAMDOC = NO 20 | WARN_AS_ERROR = YES 21 | RECURSIVE = YES 22 | EXCLUDE = CMakeLits.txt 23 | ALPHABETICAL_INDEX = NO 24 | GENERATE_HTML = YES 25 | HTML_OUTPUT = . 26 | USE_MATHJAX = NO 27 | SEARCHENGINE = NO 28 | GENERATE_LATEX = NO 29 | CLASS_DIAGRAMS = NO 30 | HIDE_UNDOC_RELATIONS = YES 31 | CLASS_GRAPH = NO 32 | COLLABORATION_GRAPH = NO 33 | GROUP_GRAPHS = NO 34 | INCLUDE_GRAPH = NO 35 | INCLUDED_BY_GRAPH = NO 36 | DIRECTORY_GRAPH = NO 37 | DISABLE_INDEX = NO 38 | GENERATE_TREEVIEW = NO 39 | HTML_DYNAMIC_SECTIONS = YES 40 | INLINE_GROUPED_CLASSES = NO 41 | INLINE_SIMPLE_STRUCTS = NO 42 | INLINE_SOURCES = NO 43 | MACRO_EXPANSION = NO 44 | VERBATIM_HEADERS = NO 45 | REFERENCES_LINK_SOURCE = NO 46 | PREDEFINED = DOXYGEN 47 | HTML_COLORSTYLE_HUE = 204 48 | HTML_COLORSTYLE_SAT = 100 49 | HTML_COLORSTYLE_GAMMA = 90 50 | HTML_EXTRA_STYLESHEET = ${CMAKE_CURRENT_SOURCE_DIR}/style.css 51 | -------------------------------------------------------------------------------- /benchmark/src/qt.cpp: -------------------------------------------------------------------------------- 1 | #include "qt.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | static auto const repeat_count = 1'000'000; 8 | static auto const sub_count = 10; 9 | 10 | void bench() 11 | { 12 | Sender sender; 13 | Receiver receiver; 14 | 15 | for(auto i = 0; i < sub_count; ++i) 16 | { 17 | auto success = receiver.connect(&sender, &Sender::inc, 18 | &receiver, &Receiver::inc, 19 | Qt::DirectConnection); 20 | (void)success; 21 | assert(success); 22 | } 23 | 24 | auto qt_duration = benchmark::time_run([&]() { emit sender.inc(1); }, repeat_count); 25 | 26 | assert(receiver.dummy == repeat_count * sub_count); 27 | receiver.dummy = 0; 28 | 29 | auto subject = observable::subject { }; 30 | 31 | for(auto i = 0; i < sub_count; ++i) 32 | subject.subscribe([&](int v) { receiver.inc(v); }).release(); 33 | 34 | auto subject_duration = benchmark::time_run([&]() { subject.notify(1); }, repeat_count); 35 | 36 | assert(receiver.dummy == repeat_count * sub_count); 37 | receiver.dummy = 0; 38 | 39 | benchmark::print("Qt signal-slot", qt_duration, "Subject", subject_duration); 40 | } 41 | 42 | int main() 43 | { 44 | for(auto i = 0; i < 5; ++i) 45 | { 46 | bench(); 47 | std::cout << std::endl; 48 | } 49 | 50 | return 0; 51 | } 52 | -------------------------------------------------------------------------------- /docs/sphinx/index.rst: -------------------------------------------------------------------------------- 1 | Observable: Generic observable objects for C++ 2 | ============================================== 3 | 4 | Observable values and expressions for C++. 5 | 6 | If you want to write code in a reactive style, this is for you. 7 | 8 | .. code-block:: C++ 9 | 10 | #include 11 | #include 12 | 13 | using namespace std; 14 | using namespace observable; 15 | 16 | int main() 17 | { 18 | auto a = value { 5 }; 19 | auto b = value { 7 }; 20 | auto avg = observe((a + b) / 2.0f); 21 | 22 | avg.subscribe([](auto const & val) { cout << val; }); 23 | 24 | b = 15; 25 | // 10 will be printed on stdout. 26 | 27 | return 0; 28 | } 29 | 30 | Check out the :doc:`getting-started` page for some more examples. 31 | 32 | The library is usable on both Windows and Linux, with any recent compiler that 33 | supports at least C++14. 34 | 35 | The library does not have any external dependencies and you don't need to 36 | compile anything (the library is header-only); so you can easily integrate it 37 | into your project. 38 | 39 | If you want to build the tests and benchmarks, they’re using CMake, so that 40 | should be easy too. 41 | 42 | .. toctree:: 43 | :maxdepth: 2 44 | :glob: 45 | 46 | * 47 | pages/index 48 | Reference 49 | Source on GitHub 50 | -------------------------------------------------------------------------------- /docs/example_tests/src/readme.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "utility.h" 5 | 6 | using namespace std::string_literals; 7 | 8 | TEST_CASE("documentation examples/readme", "[documentation examples]") 9 | { 10 | provide_cin cin_buf { "Jane"s }; 11 | capture_cout cout_buf { }; 12 | 13 | // BEGIN EXAMPLE CODE 14 | using namespace std; 15 | using namespace observable; 16 | 17 | //int main() 18 | { 19 | auto sub = subject { }; 20 | sub.subscribe([](auto const & msg) { cout << msg << endl; }); 21 | 22 | // "Hello world!" will be printed on stdout. 23 | sub.notify("Hello world!"); 24 | 25 | auto a = value { 5 }; 26 | auto b = value { 5 }; 27 | auto avg = observe( 28 | (a + b) / 2.0f 29 | ); 30 | auto eq_msg = observe( 31 | select(a == b, "equal", "not equal") 32 | ); 33 | 34 | avg.subscribe([](auto val) { cout << val << endl; }); 35 | eq_msg.subscribe([](auto const & msg) { cout << msg << endl; }); 36 | 37 | // "10" and "not equal" will be printed on stdout in an 38 | // unspecified order. 39 | b = 15; 40 | 41 | //return 0; 42 | } 43 | // END EXAMPLE CODE 44 | 45 | REQUIRE(("Hello world!\n10\nnot equal\n"s == cout_buf.str() || 46 | "Hello world!\nnot equal\n10\n"s == cout_buf.str())); 47 | } 48 | -------------------------------------------------------------------------------- /examples/capture_the_dot/README.rst: -------------------------------------------------------------------------------- 1 | Capture The Dot Example 2 | ======================= 3 | 4 | .. image:: screenshot.png 5 | :alt: Game screenshot 6 | :align: center 7 | 8 | *Capture The Dot* is a simple game where you try to click on a dot that moves 9 | to random positions. If you manage to click the dot, you have captured it and 10 | get some points. 11 | 12 | The game is built with Qt and the Observable library, to showcase how you can 13 | use observable objects to build the model part of a MVC app. 14 | 15 | The code is using a simple MVC architecture, consisting of a game controller, 16 | model and view, and has just 3 classes in total. 17 | 18 | Here's what these 3 classes do: 19 | 20 | - GameView -- Renders the game and captures user input. This is a QWidget. 21 | 22 | - GameModel -- Contains the game's business logic, like positioning 23 | computations, score and hit-test code. This class does not use Qt at all. 24 | 25 | - GameController -- Hooks up the view and the model so that the view is updated 26 | when the model changes and the model's actions are called when different 27 | events happen. 28 | 29 | Building the example 30 | -------------------- 31 | 32 | You will need Qt installed and discoverable by CMake. 33 | 34 | You can manually tell CMake about Qt by either defining the QT_ROOT variable on 35 | the command line or setting the QT_ROOT_32 or QT_ROOT_64 environment variables. 36 | 37 | If Qt is not found, the target will not be generated. 38 | 39 | Also, note that this target is compiled by default. 40 | -------------------------------------------------------------------------------- /tests/src/detail/type_traits.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace observable { namespace detail { namespace test { 7 | 8 | TEST_CASE("type_traits/is_compatible_with_observer", "[type_traits]") 9 | { 10 | SECTION("is_compatible_with_observer is true for compatible function") 11 | { 12 | auto compat = is_compatible_with_observer< 13 | void(int), 14 | subject::observer_type 15 | >::value; 16 | REQUIRE(compat); 17 | } 18 | 19 | SECTION("is_compatible_with_observer is false for incompatible function") 20 | { 21 | auto compat = is_compatible_with_observer< 22 | void(int &), 23 | subject::observer_type 24 | >::value; 25 | REQUIRE_FALSE(compat); 26 | } 27 | } 28 | 29 | TEST_CASE("type_traits/is_compatible_with_subject", "[type_traits]") 30 | { 31 | SECTION("is_compatible_with_subject is true for compatible function") 32 | { 33 | auto compat = is_compatible_with_subject>::value; 34 | REQUIRE(compat); 35 | } 36 | 37 | SECTION("is_compatible_with_subject is false for incompatible function") 38 | { 39 | auto compat = is_compatible_with_subject>::value; 40 | REQUIRE_FALSE(compat); 41 | } 42 | } 43 | 44 | } } } 45 | -------------------------------------------------------------------------------- /benchmark/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(compile_flags) 2 | include(default_source_groups) 3 | include(setup_qt) 4 | 5 | # Function benchmark target. 6 | add_executable(bench_function src/utility.h 7 | src/function.cpp) 8 | set_cpp_standard(bench_function) 9 | set_target_properties(bench_function PROPERTIES FOLDER benchmarks) 10 | target_link_libraries(bench_function observable) 11 | 12 | # Virtual function benchmark target. 13 | add_executable(bench_virtual src/utility.h 14 | src/virtual.cpp) 15 | set_cpp_standard(bench_virtual) 16 | set_target_properties(bench_virtual PROPERTIES FOLDER benchmarks) 17 | target_link_libraries(bench_virtual observable) 18 | 19 | # Expressions benchmark target 20 | add_executable(bench_expressions src/utility.h 21 | src/expressions.cpp) 22 | set_cpp_standard(bench_expressions) 23 | set_target_properties(bench_expressions PROPERTIES FOLDER benchmarks) 24 | target_link_libraries(bench_expressions observable) 25 | 26 | # Qt signal-slot benchmark target. 27 | setup_qt() 28 | find_package(Qt5 QUIET OPTIONAL_COMPONENTS Core) 29 | 30 | if(Qt5_FOUND) 31 | message(STATUS "Creating 'bench_qt' target. Using Qt5 from: ${QT_ROOT}") 32 | 33 | add_executable(bench_qt src/utility.h 34 | src/qt.h 35 | src/qt.cpp) 36 | set_cpp_standard(bench_qt) 37 | set_target_properties(bench_qt PROPERTIES FOLDER benchmarks) 38 | target_link_libraries(bench_qt observable 39 | Qt5::Core) 40 | endif() 41 | -------------------------------------------------------------------------------- /observable/include/observable/detail/compiler_config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Stringify 4 | #define OBSERVABLE_S(a) #a 5 | 6 | /** 7 | * Configure the compiler warnings. 8 | */ 9 | #if defined(__clang__) 10 | #define OBSERVABLE_BEGIN_CONFIGURE_WARNINGS \ 11 | _Pragma(OBSERVABLE_S(clang diagnostic push)) \ 12 | _Pragma(OBSERVABLE_S(clang diagnostic ignored "-Wweak-vtables")) \ 13 | _Pragma(OBSERVABLE_S(clang diagnostic ignored "-Wc++98-compat")) \ 14 | _Pragma(OBSERVABLE_S(clang diagnostic ignored "-Wpadded")) \ 15 | _Pragma(OBSERVABLE_S(clang diagnostic ignored "-Wdocumentation-unknown-command")) \ 16 | _Pragma(OBSERVABLE_S(clang diagnostic ignored "-Wglobal-constructors")) \ 17 | _Pragma(OBSERVABLE_S(clang diagnostic ignored "-Wexit-time-destructors")) 18 | #elif defined(__GNUC__) || defined(__GNUG__) 19 | #define OBSERVABLE_BEGIN_CONFIGURE_WARNINGS \ 20 | _Pragma(OBSERVABLE_S(GCC diagnostic push)) 21 | #elif defined(_MSC_VER) 22 | #define OBSERVABLE_BEGIN_CONFIGURE_WARNINGS \ 23 | __pragma(warning(push)) 24 | #endif 25 | 26 | /** 27 | * Restore the original compiler warning configuration. 28 | */ 29 | #if defined(__clang__) 30 | #define OBSERVABLE_END_CONFIGURE_WARNINGS \ 31 | _Pragma(OBSERVABLE_S(clang diagnostic pop)) 32 | #elif defined(__GNUC__) || defined(__GNUG__) 33 | #define OBSERVABLE_END_CONFIGURE_WARNINGS \ 34 | _Pragma(OBSERVABLE_S(GCC diagnostic pop)) 35 | #elif defined(_MSC_VER) 36 | #define OBSERVABLE_END_CONFIGURE_WARNINGS \ 37 | __pragma(warning(pop)) 38 | #endif 39 | -------------------------------------------------------------------------------- /benchmark/src/expressions.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utility.h" 6 | 7 | static auto const repeat_count = 100'000; 8 | static auto const loop_count = 4; 9 | volatile int dummy = 0; 10 | 11 | NOINLINE void consume(int v) { dummy += v; } 12 | 13 | void bench() 14 | { 15 | auto duration = benchmark::time_run([&]() { 16 | for(int i = 0; i < loop_count; ++i) 17 | for(int j = 0; j < loop_count; ++j) 18 | for(int k = 0; k < loop_count; ++k) 19 | consume(500 + (i * j + k) - (1 + k + j)); 20 | }, repeat_count); 21 | 22 | std::chrono::nanoseconds expr_duration; 23 | { 24 | auto i = observable::value { }; 25 | auto j = observable::value { }; 26 | auto k = observable::value { }; 27 | 28 | auto result = observable::observe(500 + (i * j * k) - (1 + k + j)); 29 | result.subscribe([](auto && x) { consume(x); }).release(); 30 | 31 | expr_duration = benchmark::time_run([&]() { 32 | for(i = 0; i.get() < loop_count; i = i.get() + 1) 33 | for(j = 0; j.get() < loop_count; j = j.get() + 1) 34 | for(k = 0; k.get() < loop_count; k = k.get() + 1) 35 | ; 36 | }, repeat_count); 37 | } 38 | 39 | benchmark::print("Normal expression", duration, "Observable expression", expr_duration); 40 | } 41 | 42 | int main() 43 | { 44 | for(auto i = 0; i < 5; ++i) 45 | { 46 | bench(); 47 | std::cout << std::endl; 48 | } 49 | 50 | return 0; 51 | } 52 | -------------------------------------------------------------------------------- /examples/capture_the_dot/src/game_view.cpp: -------------------------------------------------------------------------------- 1 | #include "game_view.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace { 7 | auto const background_color = QColor { 236, 240, 241 }; 8 | auto const regular_color = QColor { 41, 128, 185, 200 }; 9 | auto const highlight_color = QColor { 231, 76, 60, 200 }; 10 | auto const score_color = QColor { 44, 62, 80 }; 11 | 12 | auto const click_threshold = 20; // Pixels. 13 | } 14 | 15 | GameView::GameView(QWidget * parent) : 16 | QWidget(parent), 17 | color_ { regular_color } 18 | { 19 | } 20 | 21 | void GameView::highlight_dot(bool highlight) 22 | { 23 | color_ = highlight ? highlight_color : regular_color; 24 | update(); 25 | } 26 | 27 | void GameView::update_dot(double x, double y, double r) 28 | { 29 | r_ = r; 30 | x_ = x; 31 | y_ = y; 32 | update(); 33 | } 34 | 35 | void GameView::update_score(unsigned long score) 36 | { 37 | score_ = score; 38 | update(); 39 | } 40 | 41 | void GameView::mousePressEvent(QMouseEvent * ev) 42 | { 43 | click_start_ = ev->pos(); 44 | } 45 | 46 | void GameView::mouseReleaseEvent(QMouseEvent * ev) 47 | { 48 | auto const pos = ev->pos(); 49 | if((pos - click_start_).manhattanLength() <= click_threshold) 50 | emit clicked(pos.x(), pos.y()); 51 | } 52 | 53 | void GameView::resizeEvent(QResizeEvent *) 54 | { 55 | auto s = size(); 56 | emit resized(s.width(), s.height()); 57 | } 58 | 59 | void GameView::paintEvent(QPaintEvent * ev) 60 | { 61 | QPainter p { this }; 62 | p.setRenderHint(QPainter::Antialiasing); 63 | p.setBackgroundMode(Qt::OpaqueMode); 64 | p.setBackground(background_color); 65 | p.fillRect(ev->rect(), background_color); 66 | 67 | auto font = p.font(); 68 | font.setPixelSize(18); 69 | p.setFont(font); 70 | p.setPen(score_color); 71 | p.drawText(QRect { 10, 10, width() - 20, 40 }, QString { "Score: %1" }.arg(score_)); 72 | 73 | p.setPen({ QColor { 0, 0, 0, 50 }, 0.1 }); 74 | p.setBrush(color_); 75 | p.drawEllipse({ x_, y_ }, r_, r_); 76 | 77 | QWidget::paintEvent(ev); 78 | } 79 | -------------------------------------------------------------------------------- /examples/capture_the_dot/src/game_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class QMouseEvent; 5 | class QResizeEvent; 6 | class QPaintEvent; 7 | 8 | //! Render the game scene. 9 | //! 10 | //! This class is a QWidget. 11 | class GameView : public QWidget 12 | { 13 | Q_OBJECT 14 | 15 | signals: 16 | //! Signal emitted when the widget has been resized. 17 | //! 18 | //! \param width The widget's new width. 19 | //! \param height The widget's new height. 20 | void resized(double width, double height) const; 21 | 22 | //! Signal emitted when the scene is clicked. 23 | //! 24 | //! \param x The click x coordinate. 25 | //! \param y The click y coordinate. 26 | void clicked(double x, double y) const; 27 | 28 | public: 29 | //! Create a new game view with the provided parent. 30 | explicit GameView(QWidget * parent=nullptr); 31 | 32 | //! Set the dot's highlighted state. 33 | //! 34 | //! When the dot is highlighted is drawn in a different way. 35 | //! 36 | //! \param highlight True if the dot should be highlighted, false otherwise. 37 | void highlight_dot(bool highlight); 38 | 39 | //! Update the dot's position and radius. 40 | //! 41 | //! \param x The dot's x coordinate. 42 | //! \param y The dot's y coordinate. 43 | //! \param r The dot's radius. 44 | void update_dot(double x, double y, double r); 45 | 46 | //! Update the score. 47 | //! 48 | //! \param score The new score. 49 | void update_score(unsigned long score); 50 | 51 | private: 52 | //! \see QWidget::mousePressEvent 53 | void mousePressEvent(QMouseEvent * ev) override; 54 | 55 | //! \see QWidget::mouseReleaseEvent 56 | void mouseReleaseEvent(QMouseEvent * ev) override; 57 | 58 | //! \see QWidget::resizeEvent 59 | void resizeEvent(QResizeEvent * ev) override; 60 | 61 | //! \see QWidget::paintEvent 62 | void paintEvent(QPaintEvent * ev) override; 63 | 64 | private: 65 | double x_ { 0 }; 66 | double y_ { 0 }; 67 | double r_ { 0 }; 68 | QColor color_; 69 | unsigned long score_ { 0 }; 70 | 71 | QPoint click_start_; 72 | }; 73 | -------------------------------------------------------------------------------- /benchmark/src/utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #if defined(_MSC_VER) 8 | #define NOINLINE __declspec(noinline) 9 | #else 10 | #if defined(__GNUC_) 11 | #define NOINLINE __attribute__((noinline)) 12 | #else 13 | #define NOINLINE 14 | #endif 15 | #endif 16 | 17 | namespace benchmark { 18 | 19 | //! Time how long a number of calls take. 20 | //! 21 | //! \param function The function to time. 22 | //! \param repeat_count Number of times to call the function. 23 | //! \return The average time it takes for a call to return. 24 | template 25 | inline auto time_run(Function && function, unsigned long const repeat_count = 1'000'000) 26 | { 27 | auto start = std::chrono::high_resolution_clock::now(); 28 | 29 | for(auto i = repeat_count; i > 0; --i) 30 | function(); 31 | 32 | auto end = std::chrono::high_resolution_clock::now(); 33 | 34 | return (end - start) / repeat_count; 35 | } 36 | 37 | //! Compute how much slower ``slow`` is compared to ``fast``. If ``slow`` is 38 | //! actually faster than ``fast``, the result will be negative. 39 | inline auto slowdown(std::chrono::nanoseconds fast, std::chrono::nanoseconds slow) 40 | { 41 | auto f = static_cast(fast.count()); 42 | auto s = static_cast(slow.count()); 43 | 44 | return (std::max(f, s) - std::min(f, s)) / std::min(f, s) * (f < s ? 1 : -1); 45 | } 46 | 47 | //! Print timings. 48 | inline auto print(std::string first_name, std::chrono::nanoseconds first_duration, 49 | std::string second_name, std::chrono::nanoseconds second_duration) 50 | { 51 | std::cout << first_name << " run duration: " << first_duration.count() << "ns\n" 52 | << second_name << " run duration: " << second_duration.count() << "ns\n"; 53 | 54 | if(first_duration < second_duration) 55 | std::cout << second_name 56 | << " slowdown: " 57 | << slowdown(first_duration, second_duration); 58 | else 59 | std::cout << second_name 60 | << " speedup: " 61 | << slowdown(second_duration, first_duration); 62 | 63 | std::cout << std::endl; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /examples/capture_the_dot/src/game_model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | //! Capture the dot game logic. 7 | class GameModel 8 | { 9 | OBSERVABLE_PROPERTIES(GameModel) 10 | 11 | public: 12 | struct Point { double x; double y; }; 13 | 14 | //! Position of the dot. 15 | observable_property dot_position; 16 | 17 | //! Dot's radius. 18 | observable_property dot_radius { 10 }; 19 | 20 | //! True if the dot has been captured, false otherwise. 21 | observable_property is_captured { false }; 22 | 23 | //! The game's current score. 24 | observable_property score { 0 }; 25 | 26 | //! Minimum allowed scene width. 27 | observable_property min_scene_width; 28 | 29 | //! Minimum allowed scene height. 30 | observable_property min_scene_height; 31 | 32 | //! Current scene width. 33 | observable_property scene_width; 34 | 35 | //! Current scene height. 36 | observable_property scene_height; 37 | 38 | //! The current delay between random moves. 39 | observable_property delay; 40 | 41 | public: 42 | //! Create a game model. 43 | GameModel(); 44 | 45 | //! Try to capture the dot for a current position. 46 | //! 47 | //! If the current position is intersecting the dot, the ``is_captured`` 48 | //! property will become true. 49 | void try_capture(Point const & cursor_position); 50 | 51 | //! Move the dot to a random position and change it's radius. 52 | //! 53 | //! The dot will be inside the scene as defined by the ``scene_width`` and 54 | //! ``scene_height`` properties. 55 | //! 56 | //! A call to this method will also reset the ``is_capture`` property if it 57 | //! has been set by try_capture(). 58 | //! 59 | //! You should call this method continuously, with a delay prescribed by the 60 | //! ``delay`` property between two successive calls. 61 | void randomize_dot(); 62 | 63 | //! Resize the scene. 64 | //! 65 | //! This will affect both the ``scene_width`` and ``scene_height`` properties 66 | //! as well as the ``dot_position`` property. 67 | void resize_scene(double width, double height); 68 | 69 | private: 70 | observable::value pos_ { { 250, 250 } }; 71 | observable::value width_ { 500 }; 72 | observable::value height_ { 500 }; 73 | 74 | std::random_device rd_; 75 | }; 76 | -------------------------------------------------------------------------------- /docs/doxygen/DoxygenLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/capture_the_dot/src/game_controller.cpp: -------------------------------------------------------------------------------- 1 | #include "game_controller.h" 2 | #include 3 | #include 4 | #include "game_view.h" 5 | #include "game_model.h" 6 | 7 | using namespace std::chrono_literals; 8 | 9 | GameController::GameController(std::shared_ptr view, 10 | std::shared_ptr model) : 11 | view_ { std::move(view) }, 12 | model_ { std::move(model) } 13 | { 14 | assert(view_); 15 | assert(model_); 16 | 17 | // Link the model to the view. 18 | 19 | cons_.emplace_back( 20 | QObject::connect(view_.get(), &GameView::resized, 21 | [&](auto w, auto h) { model_->resize_scene(w, h); }) 22 | ); 23 | 24 | cons_.emplace_back( 25 | QObject::connect(view_.get(), &GameView::clicked, 26 | [&](auto x, auto y) { model_->try_capture({ x, y }); }) 27 | ); 28 | 29 | // Link the view to the model. 30 | 31 | subs_.emplace_back(model_->is_captured.subscribe([&](auto captured) { 32 | view_->highlight_dot(captured); 33 | })); 34 | 35 | subs_.emplace_back(model_->dot_position.subscribe([&](auto const & pos) { 36 | view_->update_dot(pos.x, pos.y, model_->dot_radius.get()); 37 | })); 38 | 39 | subs_.emplace_back(model_->dot_radius.subscribe([&](auto r) { 40 | auto const pos = model_->dot_position.get(); 41 | view_->update_dot(pos.x, pos.y, r); 42 | })); 43 | 44 | subs_.emplace_back(model_->min_scene_width.subscribe([&](auto w) { 45 | view_->setMinimumWidth(w); 46 | })); 47 | 48 | subs_.emplace_back(model_->min_scene_height.subscribe([&](auto h) { 49 | view_->setMinimumHeight(h); 50 | })); 51 | 52 | subs_.emplace_back(model_->score.subscribe([&](auto s) { 53 | view_->update_score(s); 54 | })); 55 | 56 | // Setup the game timer links. 57 | 58 | cons_.emplace_back( 59 | QObject::connect(timer_.get(), &QTimer::timeout, 60 | [&]() { model_->randomize_dot(); }) 61 | ); 62 | 63 | subs_.emplace_back(model_->delay.subscribe([&](auto d) { 64 | using namespace std::chrono; 65 | timer_->start(duration_cast(d).count()); 66 | })); 67 | } 68 | 69 | void GameController::start() 70 | { 71 | using namespace std::chrono; 72 | timer_->start(duration_cast(model_->delay.get()).count()); 73 | model_->randomize_dot(); 74 | } 75 | 76 | void GameController::stop() 77 | { 78 | timer_->stop(); 79 | } 80 | 81 | GameController::~GameController() 82 | { 83 | stop(); 84 | 85 | for(auto && c : cons_) 86 | QObject::disconnect(c); 87 | } 88 | -------------------------------------------------------------------------------- /tests/src/infinite_subscription.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace observable { namespace test { 7 | 8 | TEST_CASE("infinite_subscription/creation", "[infinite_subscription]") 9 | { 10 | SECTION("is default-constructible") 11 | { 12 | REQUIRE(std::is_default_constructible::value); 13 | } 14 | 15 | SECTION("can create an initialized subscription") 16 | { 17 | infinite_subscription { []() {} }; 18 | } 19 | } 20 | 21 | TEST_CASE("infinite_subscription/unsubscribing", "[infinite_subscription]") 22 | { 23 | SECTION("unsubscribe function is called") 24 | { 25 | auto call_count = 0; 26 | auto sub = infinite_subscription { [&]() { ++call_count; } }; 27 | 28 | sub.unsubscribe(); 29 | 30 | REQUIRE(call_count == 1); 31 | } 32 | 33 | SECTION("destructor does not call unsubscribe function") 34 | { 35 | auto call_count = 0; 36 | 37 | { 38 | infinite_subscription { [&]() { ++call_count; } }; 39 | } 40 | 41 | REQUIRE(call_count == 0); 42 | } 43 | 44 | SECTION("calling unsubscribe() multiple times only calls function once") 45 | { 46 | auto call_count = 0; 47 | 48 | auto sub = infinite_subscription { [&]() { ++call_count; } }; 49 | sub.unsubscribe(); 50 | sub.unsubscribe(); 51 | 52 | REQUIRE(call_count == 1); 53 | } 54 | 55 | SECTION("unsubscribing from moved subscription calls function") 56 | { 57 | auto call_count = 0; 58 | infinite_subscription other; 59 | 60 | { 61 | auto sub = infinite_subscription { [&]() { ++call_count; } }; 62 | other = std::move(sub); 63 | } 64 | 65 | other.unsubscribe(); 66 | 67 | REQUIRE(call_count == 1); 68 | } 69 | } 70 | 71 | TEST_CASE("infinite_subscription/copy", "[infinite_subscription]") 72 | { 73 | SECTION("is not copy-constructible") 74 | { 75 | REQUIRE_FALSE(std::is_copy_constructible::value); 76 | } 77 | 78 | SECTION("is not copy-assignable") 79 | { 80 | REQUIRE_FALSE(std::is_copy_assignable::value); 81 | } 82 | } 83 | 84 | TEST_CASE("infinite_subscription/move", "[infinite_subscription]") 85 | { 86 | SECTION("is move-constructible") 87 | { 88 | REQUIRE(std::is_move_constructible::value); 89 | } 90 | 91 | SECTION("is move-assignable") 92 | { 93 | REQUIRE(std::is_move_assignable::value); 94 | } 95 | } 96 | 97 | } } 98 | -------------------------------------------------------------------------------- /observable/include/observable/detail/type_traits.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include 6 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 7 | 8 | namespace observable { namespace detail { 9 | 10 | //! Check if a callable type is compatible with an observer type. 11 | //! 12 | //! A callable is compatible with an observer type if the callable can be stored 13 | //! inside a `std::function`. 14 | //! 15 | //! \tparam CallableType The type to check if it is compatible with the 16 | //! ObserverType. 17 | //! \tparam ObserverType The type of the observer to check against. 18 | //! 19 | //! The static member ``value`` will be true, if the CallableType is compatible 20 | //! with the ObserverType. 21 | //! 22 | //! \ingroup observable_detail 23 | template 24 | using is_compatible_with_observer = std::is_convertible< 25 | CallableType, 26 | std::function>; 27 | 28 | //! Check if a callable type can be used to subscribe to a subject. 29 | //! 30 | //! A callable can be used to subscribe to a subject if the callable is compatible 31 | //! with the subject's ``observer_type`` type. 32 | //! 33 | //! \tparam CallableType The type to check if it can be used to subscribe to the 34 | //! provided subject. 35 | //! \tparam SubjectType The subject to check against. 36 | //! 37 | //! The static member ``value`` will be true if, the CallableType can be used 38 | //! with the SubjectType. 39 | //! 40 | //! \ingroup observable_detail 41 | template 42 | using is_compatible_with_subject = is_compatible_with_observer< 43 | CallableType, 44 | typename SubjectType::observer_type>; 45 | 46 | //! Check if two types are equality comparable between themselves. 47 | template 48 | struct are_equality_comparable : std::false_type 49 | { }; 50 | 51 | //! \cond 52 | template 53 | struct are_equality_comparable { }(std::declval(), 57 | std::declval())) 58 | >, bool>::value> 59 | > : std::true_type 60 | { 61 | }; 62 | //! \endcond 63 | 64 | } } 65 | 66 | OBSERVABLE_END_CONFIGURE_WARNINGS 67 | -------------------------------------------------------------------------------- /docs/example_tests/src/using_expressions.cpp: -------------------------------------------------------------------------------- 1 | #define _USE_MATH_DEFINES 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace std::string_literals; 8 | 9 | namespace { 10 | double square_(double val) { return std::pow(val, 2); } 11 | OBSERVABLE_ADAPT_FILTER(square, square_) 12 | } 13 | 14 | TEST_CASE("documentation examples/using expressions", "[documentation examples]") 15 | { 16 | SECTION("simple expressions") 17 | { 18 | // BEGIN EXAMPLE CODE 19 | 20 | //int main() 21 | { 22 | auto radius = observable::value { 5 }; 23 | auto circumference = observe(2 * M_PI * radius); 24 | 25 | assert(fabs(circumference.get() - 2 * M_PI * 5) < 0.001); 26 | 27 | radius = 7; 28 | 29 | assert(fabs(circumference.get() - 2 * M_PI * 7) < 0.001); 30 | } 31 | // END EXAMPLE CODE 32 | } 33 | 34 | SECTION("with updater") 35 | { 36 | // BEGIN EXAMPLE CODE 37 | 38 | //int main() 39 | { 40 | auto radius = observable::value { 5 }; 41 | 42 | auto updater = observable::updater { }; 43 | auto circumference = observe(updater, 2 * M_PI * radius); 44 | 45 | assert(fabs(circumference.get() - 2 * M_PI * 5) < 0.001); 46 | 47 | radius = 7; 48 | assert(fabs(circumference.get() - 2 * M_PI * 5) < 0.001); 49 | 50 | updater.update_all(); 51 | assert(fabs(circumference.get() - 2 * M_PI * 7) < 0.001); 52 | } 53 | // END EXAMPLE CODE 54 | } 55 | 56 | SECTION("predefined filters") 57 | { 58 | // BEGIN EXAMPLE CODE 59 | 60 | //int main() 61 | { 62 | auto radius = observable::value { 5 }; 63 | auto area = observe(M_PI * pow(radius, 2)); 64 | auto is_large = observe(select(area > 100, true, false)); 65 | 66 | assert(fabs(area.get() - M_PI * std::pow(5, 2)) < 0.001); 67 | assert(is_large.get() == false); 68 | 69 | radius = 70; 70 | 71 | assert(fabs(area.get() - M_PI * std::pow(70, 2)) < 0.001); 72 | assert(is_large.get() == true); 73 | } 74 | // END EXAMPLE CODE 75 | } 76 | 77 | SECTION("user filters") 78 | { 79 | // BEGIN EXAMPLE CODE 80 | 81 | //int main() 82 | { 83 | auto radius = observable::value { 5 }; 84 | auto area = observe(M_PI * square(radius)); 85 | 86 | assert(fabs(area.get() - M_PI * std::pow(5, 2)) < 0.001); 87 | 88 | radius = 70; 89 | 90 | assert(fabs(area.get() - M_PI * std::pow(70, 2)) < 0.001); 91 | } 92 | // END EXAMPLE CODE 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/src/shared_subscription.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace observable { namespace test { 7 | 8 | TEST_CASE("shared_subscripton/creation", "[shared_subscription]") 9 | { 10 | SECTION("shared subscriptions are default-constructible") 11 | { 12 | REQUIRE(std::is_nothrow_default_constructible::value); 13 | } 14 | 15 | SECTION("can create a shared_subscription from an infinite_subscription") 16 | { 17 | shared_subscription { infinite_subscription { []() {} } }; 18 | } 19 | 20 | SECTION("can create a shared_subscription from an unique_subscription") 21 | { 22 | shared_subscription { 23 | unique_subscription { 24 | infinite_subscription { []() {} } 25 | } 26 | }; 27 | } 28 | } 29 | 30 | TEST_CASE("shared_subscripton/unsubscribing", "[shared_subscription]") 31 | { 32 | SECTION("unsubscribe is called when destroyed") 33 | { 34 | auto call_count = 0; 35 | 36 | { 37 | shared_subscription { infinite_subscription { [&]() { ++call_count; } } }; 38 | } 39 | 40 | REQUIRE(call_count == 1); 41 | } 42 | 43 | SECTION("can manually call unsubscribe()") 44 | { 45 | auto call_count = 0; 46 | 47 | auto sub = shared_subscription { infinite_subscription { [&]() { ++call_count; } } }; 48 | sub.unsubscribe(); 49 | 50 | REQUIRE(call_count == 1); 51 | } 52 | 53 | SECTION("unsubscribe is called by last instance when destroyed") 54 | { 55 | auto call_count = 0; 56 | 57 | { 58 | auto sub = shared_subscription { 59 | infinite_subscription { [&]() { ++call_count; } } 60 | }; 61 | 62 | { 63 | auto copy = sub; 64 | } 65 | 66 | REQUIRE(call_count == 0); 67 | } 68 | 69 | REQUIRE(call_count == 1); 70 | } 71 | } 72 | 73 | TEST_CASE("shared_subscripton/copying", "[shared_subscription]") 74 | { 75 | SECTION("shared subscriptions are copy-constructible") 76 | { 77 | REQUIRE(std::is_copy_constructible::value); 78 | } 79 | 80 | SECTION("shared subscriptions are copy-assignable") 81 | { 82 | REQUIRE(std::is_copy_assignable::value); 83 | } 84 | } 85 | 86 | TEST_CASE("shared_subscripton/moving", "[shared_subscripton]") 87 | { 88 | SECTION("shared subscriptions are move-constructible") 89 | { 90 | REQUIRE(std::is_move_constructible::value); 91 | } 92 | 93 | SECTION("shared subscriptions are move-assignable") 94 | { 95 | REQUIRE(std::is_move_assignable::value); 96 | } 97 | } 98 | 99 | } } 100 | -------------------------------------------------------------------------------- /tests/src/unique_subscription.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace observable { namespace test { 7 | 8 | TEST_CASE("unique_subscription/creation", "[unique_subscription]") 9 | { 10 | SECTION("unique subscriptions are default-constructible") 11 | { 12 | REQUIRE(std::is_default_constructible::value); 13 | } 14 | 15 | SECTION("can create initialized subscription") 16 | { 17 | unique_subscription { infinite_subscription { []() {} } }; 18 | } 19 | } 20 | 21 | TEST_CASE("unique_subscription/unsubscribing", "[unique_subscription]") 22 | { 23 | SECTION("unsubscribe function is called") 24 | { 25 | auto call_count = 0; 26 | auto sub = unique_subscription { 27 | infinite_subscription { [&]() { ++call_count; } } 28 | }; 29 | 30 | sub.unsubscribe(); 31 | 32 | REQUIRE(call_count == 1); 33 | } 34 | 35 | SECTION("destructor calls unsubscribe function") 36 | { 37 | auto call_count = 0; 38 | 39 | { 40 | unique_subscription { 41 | infinite_subscription { [&]() { ++call_count; } } 42 | }; 43 | } 44 | 45 | REQUIRE(call_count == 1); 46 | } 47 | 48 | SECTION("calling unsubscribe multiple times calls function once") 49 | { 50 | auto call_count = 0; 51 | 52 | auto sub = unique_subscription { 53 | infinite_subscription { [&]() { ++call_count; } } 54 | }; 55 | sub.unsubscribe(); 56 | sub.unsubscribe(); 57 | 58 | REQUIRE(call_count == 1); 59 | } 60 | } 61 | 62 | TEST_CASE("unique_subscription/copying", "[unique_subscription]") 63 | { 64 | SECTION("unique subscriptions are not copy-constructible") 65 | { 66 | REQUIRE_FALSE(std::is_copy_constructible::value); 67 | } 68 | 69 | SECTION("unique subscriptions are not copy-assignable") 70 | { 71 | REQUIRE_FALSE(std::is_copy_assignable::value); 72 | } 73 | } 74 | 75 | TEST_CASE("unique_subscription/moving", "[unique_subscription]") 76 | { 77 | SECTION("unique subscriptions are move-constructible") 78 | { 79 | REQUIRE(std::is_move_constructible::value); 80 | } 81 | 82 | SECTION("unique subscriptions are move-assignable") 83 | { 84 | REQUIRE(std::is_move_assignable::value); 85 | } 86 | 87 | SECTION("unsubscribing from moved subscription calls function") 88 | { 89 | auto call_count = 0; 90 | unique_subscription other; 91 | 92 | { 93 | auto sub = unique_subscription { 94 | infinite_subscription { [&]() { ++call_count; } } 95 | }; 96 | other = std::move(sub); 97 | } 98 | 99 | other.unsubscribe(); 100 | 101 | REQUIRE(call_count == 1); 102 | } 103 | } 104 | 105 | } } 106 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | cache: 4 | apt: true 5 | 6 | addons: 7 | apt: 8 | sources: &sources 9 | - ubuntu-toolchain-r-test 10 | - llvm-toolchain-precise 11 | - llvm-toolchain-precise-3.6 12 | - llvm-toolchain-precise-3.8 13 | 14 | matrix: 15 | include: 16 | - env: CC=gcc-7 CXX=g++-7 STD=14 17 | os: linux 18 | dist: trusty 19 | sudo: require 20 | addons: 21 | apt: 22 | sources: *sources 23 | packages: 24 | - gcc-7 25 | - g++-7 26 | 27 | - env: CC=gcc-7 CXX=g++-7 STD=17 28 | os: linux 29 | dist: trusty 30 | sudo: require 31 | addons: 32 | apt: 33 | sources: *sources 34 | packages: 35 | - gcc-7 36 | - g++-7 37 | 38 | - env: CC=gcc-6 CXX=g++-6 STD=14 39 | os: linux 40 | dist: trusty 41 | sudo: require 42 | addons: 43 | apt: 44 | sources: *sources 45 | packages: 46 | - gcc-6 47 | - g++-6 48 | 49 | - env: CC=gcc-6 CXX=g++-6 STD=17 50 | os: linux 51 | dist: trusty 52 | sudo: require 53 | addons: 54 | apt: 55 | sources: *sources 56 | packages: 57 | - gcc-6 58 | - g++-6 59 | 60 | - env: CC=gcc-5 CXX=g++-5 STD=14 61 | os: linux 62 | dist: trusty 63 | sudo: require 64 | addons: 65 | apt: 66 | sources: *sources 67 | packages: 68 | - gcc-5 69 | - g++-5 70 | 71 | - env: CC=gcc-5 CXX=g++-5 STD=17 72 | os: linux 73 | dist: trusty 74 | sudo: require 75 | addons: 76 | apt: 77 | sources: *sources 78 | packages: 79 | - gcc-5 80 | - g++-5 81 | 82 | - env: CC=clang-3.8 CXX=clang++-3.8 STD=14 83 | os: linux 84 | dist: trusty 85 | sudo: require 86 | addons: 87 | apt: 88 | sources: *sources 89 | packages: 90 | - g++-6 # Hack to make clang work. 91 | - clang-3.8 92 | 93 | - env: CC=clang-3.8 CXX=clang++-3.8 STD=17 94 | os: linux 95 | dist: trusty 96 | sudo: require 97 | addons: 98 | apt: 99 | sources: *sources 100 | packages: 101 | - g++-6 # Hack to make clang work. 102 | - clang-3.8 103 | 104 | - env: CC=clang-3.6 CXX=clang++-3.6 STD=14 105 | os: linux 106 | dist: trusty 107 | sudo: require 108 | addons: 109 | apt: 110 | sources: *sources 111 | packages: 112 | - clang-3.6 113 | 114 | - env: CC=clang-3.6 CXX=clang++-3.6 STD=17 115 | os: linux 116 | dist: trusty 117 | sudo: require 118 | addons: 119 | apt: 120 | sources: *sources 121 | packages: 122 | - clang-3.6 123 | 124 | - env: STD=14 125 | os: osx 126 | osx_image: xcode8.2 127 | 128 | - env: STD=17 129 | os: osx 130 | osx_image: xcode8.2 131 | 132 | script: 133 | - mkdir build && cd build 134 | - cmake -DCPP_STANDARD=${STD} .. 135 | - cmake --build . --config debug 136 | - ctest -V -C debug 137 | -------------------------------------------------------------------------------- /tests/src/expressions/operators.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace { // Operators should be usable with ADL. 8 | 9 | using observable::value; 10 | using observable::expression_node; 11 | 12 | #define MAKE_UNARY_OP_TEST(NAME, OP, V) \ 13 | TEST_CASE("expression operator/" NAME, "[expression operator]") \ 14 | { \ 15 | auto v = value { V }; \ 16 | \ 17 | auto const expected = OP V; \ 18 | \ 19 | REQUIRE((OP v).get() == expected); \ 20 | REQUIRE((OP expression_node { V }).get() == expected); \ 21 | \ 22 | struct enclosing { \ 23 | value v { V }; \ 24 | } enc ; \ 25 | \ 26 | REQUIRE((OP enc.v).get() == expected); \ 27 | } 28 | 29 | MAKE_UNARY_OP_TEST("!", !, false) 30 | MAKE_UNARY_OP_TEST("~", ~, 0xb0101) 31 | MAKE_UNARY_OP_TEST("unary +", +, 5) 32 | MAKE_UNARY_OP_TEST("unary -", -, 5) 33 | 34 | #define MAKE_BINARY_OP_TEST(NAME, A, OP, B) \ 35 | TEST_CASE("expression operator/" NAME, "[expression operator]") \ 36 | { \ 37 | auto va = value { A }; \ 38 | auto vb = value { B }; \ 39 | \ 40 | auto const expected = A OP B; \ 41 | \ 42 | REQUIRE((va OP B).get() == expected); \ 43 | REQUIRE((A OP vb).get() == expected); \ 44 | REQUIRE((va OP vb).get() == expected); \ 45 | REQUIRE((expression_node { A } OP B).get() == expected); \ 46 | REQUIRE((A OP expression_node { B }).get() == expected); \ 47 | REQUIRE((expression_node { A } OP expression_node { B }).get() == expected); \ 48 | REQUIRE((va OP expression_node { B }).get() == expected); \ 49 | REQUIRE((expression_node { A } OP vb).get() == expected); \ 50 | \ 51 | struct enclosing { \ 52 | value va { A }; \ 53 | value vb { B }; \ 54 | } enc; \ 55 | \ 56 | REQUIRE((enc.va OP B).get() == expected); \ 57 | REQUIRE((A OP enc.vb).get() == expected); \ 58 | REQUIRE((enc.va OP enc.vb).get() == expected); \ 59 | REQUIRE((enc.va OP expression_node { B }).get() == expected); \ 60 | REQUIRE((expression_node { A } OP enc.vb).get() == expected); \ 61 | } 62 | 63 | MAKE_BINARY_OP_TEST("*", 5, *, 7) 64 | MAKE_BINARY_OP_TEST("/", 10, /, 2) 65 | MAKE_BINARY_OP_TEST("%", 5, %, 2) 66 | MAKE_BINARY_OP_TEST("+", 5, +, 3) 67 | MAKE_BINARY_OP_TEST("-", 5, -, 3) 68 | MAKE_BINARY_OP_TEST("<<", 0x1, <<, 2) 69 | MAKE_BINARY_OP_TEST(">>", 0xF0, >>, 2) 70 | MAKE_BINARY_OP_TEST("<", 2, <, 5) 71 | MAKE_BINARY_OP_TEST("<=", 2, <=, 5) 72 | MAKE_BINARY_OP_TEST(">", 5, >, 2) 73 | MAKE_BINARY_OP_TEST(">=", 5, >=, 2) 74 | MAKE_BINARY_OP_TEST("==", 7, ==, 7) 75 | MAKE_BINARY_OP_TEST("!=", 7, !=, 3) 76 | MAKE_BINARY_OP_TEST("&", 0x1, &, 0x1) 77 | MAKE_BINARY_OP_TEST("^", 0x1, ^, 0x2) 78 | MAKE_BINARY_OP_TEST("|", 0x1, |, 0x2) 79 | MAKE_BINARY_OP_TEST("&&", true, &&, true) 80 | MAKE_BINARY_OP_TEST("||", true, ||, false) 81 | 82 | TEST_CASE("expression operator/nodes are updated", "[expression operator]") 83 | { 84 | auto a = value { 5 }; 85 | auto b = value { 9 }; 86 | auto c = value { 10 }; 87 | 88 | auto r = ((a + b) * c) / 2.5f; 89 | 90 | a = 7; 91 | b = 13; 92 | c = 15; 93 | 94 | r.eval(); 95 | 96 | REQUIRE(r.get() == Approx { ((a.get() + b.get()) * c.get()) / 2.5f }); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /examples/capture_the_dot/src/game_model.cpp: -------------------------------------------------------------------------------- 1 | #include "game_model.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace { 7 | constexpr auto const max_radius = 50.0; 8 | constexpr auto const padding = 10.0; 9 | 10 | //! Constrain the provided point to a rectangle. 11 | GameModel::Point captive_(GameModel::Point const & p, 12 | double min_x, double max_x, 13 | double min_y, double max_y) 14 | { 15 | return { 16 | std::min(std::max(p.x, min_x), max_x), 17 | std::min(std::max(p.y, min_y), max_y) 18 | }; 19 | } 20 | 21 | OBSERVABLE_ADAPT_FILTER(captive, captive_) 22 | 23 | //! Compute the distance between two points. 24 | double distance(GameModel::Point const & p1, GameModel::Point const & p2) 25 | { 26 | return std::sqrt(std::pow(p1.x - p2.x, 2) + std::pow(p1.y - p2.y, 2)); 27 | } 28 | } 29 | 30 | GameModel::GameModel() 31 | { 32 | min_scene_width = observe(dot_radius * 2 + padding); 33 | 34 | min_scene_height = observe(dot_radius * 2 + padding); 35 | 36 | scene_width = observe(max(min_scene_width, width_)); 37 | 38 | scene_height = observe(max(min_scene_height, height_)); 39 | 40 | dot_position = observe( 41 | captive(pos_, 42 | dot_radius + padding, 43 | scene_width - dot_radius - padding, 44 | dot_radius + padding, 45 | scene_height - dot_radius - padding) 46 | ); 47 | 48 | using namespace std::chrono_literals; 49 | using namespace std::chrono; 50 | using observable::static_expr_cast; 51 | using observable::construct; 52 | 53 | delay = observe( 54 | select(is_captured, 55 | 500ms, 56 | construct(clamp( 57 | // Decrease the delay logarithmically as the score 58 | // increases. The magic numbers are empirical values 59 | // that yield a decent delay curve. 60 | static_expr_cast(2200 / log10(40 + score / 3)), 61 | 600, 62 | 1500 63 | )) 64 | ) 65 | ); 66 | 67 | is_captured.subscribe([this](auto captured) { 68 | if(!captured) 69 | return; 70 | 71 | // The harder it is to click on the dot (i.e. small radius or big 72 | // scene), the more points you should get. 73 | auto const k = 1 - dot_radius.get() / max_radius; 74 | auto const a = scene_width.get() * scene_height.get(); 75 | auto const n = std::pow(k * 10, 2) + a / 10000; 76 | score = score.get() + static_cast(n); 77 | }).release(); 78 | } 79 | 80 | void GameModel::try_capture(Point const & cursor_position) 81 | { 82 | is_captured = distance(cursor_position, dot_position.get()) <= dot_radius.get(); 83 | } 84 | 85 | void GameModel::randomize_dot() 86 | { 87 | is_captured = false; 88 | 89 | auto xs = std::uniform_real_distribution { 0, scene_width.get() }; 90 | auto ys = std::uniform_real_distribution { 0, scene_height.get() }; 91 | auto rs = std::uniform_real_distribution { max_radius / 4, max_radius }; 92 | 93 | pos_ = Point { xs(rd_), ys(rd_) }; 94 | dot_radius = rs(rd_); 95 | } 96 | 97 | void GameModel::resize_scene(double width, double height) 98 | { 99 | assert(width > 0); 100 | assert(height > 0); 101 | 102 | width_ = width; 103 | height_ = height; 104 | } 105 | -------------------------------------------------------------------------------- /cmake/compile_flags.cmake: -------------------------------------------------------------------------------- 1 | # Enable strict warnings in both debug and release modes. Treat warnings as 2 | # errors in debug mode. 3 | # 4 | # - target_name Name of the configured target. 5 | function(enable_strict_warnings target_name) 6 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) 7 | target_compile_options(${target_name} 8 | PRIVATE 9 | /Wall /wd4820 /wd4514 /wd4625 /wd4626 /wd5026 /wd5027 /wd4710 10 | /wd4571 /wd5039 /wd4623 /wd4774 /wd4548 /wd4711 /wd4868 /wd5045 11 | /wd4628 12 | $<$:/WX> 13 | ) 14 | elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL GNU) 15 | target_compile_options(${target_name} 16 | PRIVATE 17 | -Wall -Werror -Wextra -pedantic -Wshadow 18 | ) 19 | elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL Clang) 20 | target_compile_options(${target_name} 21 | PRIVATE 22 | -Werror -Weverything -Wno-system-headers -Wno-float-equal 23 | -Wno-reserved-id-macro -Wno-padded -Wno-c++98-compat 24 | -Wno-global-constructors -Wno-missing-prototypes 25 | -Wno-unused-macros -Wno-disabled-macro-expansion 26 | ) 27 | else() 28 | message(WARNING "Custom compiler flags not set.\n" 29 | "Your compiler is not supported.\n" 30 | "Detected id is '${CMAKE_CXX_COMPILER_ID}'.") 31 | endif() 32 | endfunction(enable_strict_warnings) 33 | 34 | # Disablee all warnings. 35 | # 36 | # - target_name Name of the configured target. 37 | function(disable_warnings target_name) 38 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) 39 | target_compile_options(${target_name} PRIVATE /W0) 40 | elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL GNU) 41 | target_compile_options(${target_name} PRIVATE -w) 42 | elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL Clang) 43 | target_compile_options(${target_name} PRIVATE -w) 44 | endif() 45 | endfunction(disable_warnings) 46 | 47 | # Set the default compile flags. 48 | # 49 | # - target_name Name of the configured target. 50 | function(set_default_flags target_name) 51 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) 52 | set(flags /MP /GF) 53 | elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL GNU) 54 | set(flags -fpic) 55 | elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL Clang) 56 | set(flags -fpic) 57 | else() 58 | message(WARNING "Custom compiler flags not set.\n" 59 | "Your compiler is not supported.\n" 60 | "Detected id is '${CMAKE_CXX_COMPILER_ID}'.") 61 | endif() 62 | 63 | target_compile_options(${target_name} PRIVATE ${flags}) 64 | endfunction(set_default_flags) 65 | 66 | # Set the required C++ standard version (ie. C++14). 67 | # 68 | # - target_name Name of the configured target. 69 | function(set_cpp_standard target_name) 70 | get_property(cpp_standard GLOBAL PROPERTY cpp_standard) 71 | 72 | if(NOT CPP_STANDARD AND NOT cpp_standard) 73 | set(cpp_standard 14) 74 | message(STATUS "You can set the C++ standard by defining CPP_STANDARD") 75 | elseif(NOT cpp_standard) 76 | set(cpp_standard ${CPP_STANDARD}) 77 | endif() 78 | set_property(GLOBAL PROPERTY cpp_standard ${cpp_standard}) 79 | 80 | get_property(cpp_standard_message_printed 81 | GLOBAL 82 | PROPERTY cpp_standard_message_printed) 83 | if(NOT cpp_standard_message_printed) 84 | message(STATUS "Using the C++${cpp_standard} standard") 85 | set_property(GLOBAL PROPERTY cpp_standard_message_printed TRUE) 86 | endif() 87 | 88 | set_property(TARGET ${target_name} PROPERTY CXX_STANDARD ${cpp_standard}) 89 | set_property(TARGET ${target_name} PROPERTY CXX_STANDARD_REQUIRED on) 90 | endfunction(set_cpp_standard) 91 | 92 | # Set the default compiler flags. 93 | # 94 | # - target_name Name of the configured target. 95 | function(configure_compiler target_name) 96 | enable_strict_warnings(${target_name}) 97 | set_cpp_standard(${target_name}) 98 | set_default_flags(${target_name}) 99 | endfunction(configure_compiler) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Observable: Generic observable objects for C++ 2 | ============================================== 3 | 4 | Write declarative, reactive expressions or just implement the observer pattern. 5 | 6 | Observable is a self-contained, header-only library that has no depencencies. 7 | Drop it somewhere in your include path and you're good to go. 8 | 9 | Quick start 10 | ----------- 11 | 12 | Example: 13 | 14 | .. code:: C++ 15 | 16 | #include 17 | #include 18 | 19 | using namespace std; 20 | using namespace observable; 21 | 22 | int main() 23 | { 24 | auto sub = subject { }; 25 | sub.subscribe([](auto const & msg) { cout << msg << endl; }); 26 | 27 | // "Hello, world!" will be printed to stdout. 28 | sub.notify("Hello, world!"); 29 | 30 | auto a = value { 5 }; 31 | auto b = value { 5 }; 32 | auto avg = observe( 33 | (a + b) / 2.0f 34 | ); 35 | auto eq_msg = observe( 36 | select(a == b, "equal", "not equal") 37 | ); 38 | 39 | avg.subscribe([](auto val) { cout << val << endl; }); 40 | eq_msg.subscribe([](auto const & msg) { cout << msg << endl; }); 41 | 42 | // "10" and "not equal" will be printed to stdout in an 43 | // unspecified order. 44 | b = 15; 45 | 46 | return 0; 47 | } 48 | 49 | Documentation 50 | ------------- 51 | 52 | You can `access the documentation `_ here: 53 | https://danieldinu.com/observable/. 54 | 55 | What's with the CMake files? 56 | ---------------------------- 57 | 58 | The library uses CMake to build the tests, benchmarks and documentation. You 59 | do not need CMake if you don't plan on running the tests or benchmarks. 60 | 61 | Contributing 62 | ------------ 63 | 64 | Bug reports, feature requests, documentation and code contributions are welcome 65 | and highly appreciated. Please open an issue or feature request before you 66 | start working on any pull request. 67 | 68 | Legal and Licensing 69 | ------------------- 70 | 71 | The library is licensed under the `Apache License version 2.0 `_. 72 | 73 | All contributions must be provided under the terms of this license. 74 | 75 | Supported compilers 76 | ------------------- 77 | 78 | Any relatively recent compiler with C++14 support should work. 79 | 80 | The code has been tested with the following compilers: 81 | 82 | * MSVC 15 (Visual Studio 2017) 83 | * MSVC 14 (Visual Studio 2015) 84 | * GCC 5, 6, 7 85 | * Clang 3.6, 3.8 86 | * AppleClang 9.1 87 | 88 | Build status 89 | ------------ 90 | 91 | Visual Studio 2017 builds: 92 | 93 | * |win32 15 build C++14|_ (32 bit, C++14) 94 | * |win64 15 build C++14|_ (64 bit, C++14) 95 | * |win32 15 build C++17|_ (32 bit, C++17) 96 | * |win64 15 build C++17|_ (64 bit, C++17) 97 | 98 | .. |win32 15 build C++14| image:: https://ci.appveyor.com/api/projects/status/sgomsxwri8wknode?svg=true 99 | .. _win32 15 build C++14: https://ci.appveyor.com/project/ddinu/observable-crrsf 100 | 101 | .. |win64 15 build C++14| image:: https://ci.appveyor.com/api/projects/status/tpr4qem5gxo7dntb?svg=true 102 | .. _win64 15 build C++14: https://ci.appveyor.com/project/ddinu/observable-uyjd7 103 | 104 | .. |win32 15 build C++17| image:: https://ci.appveyor.com/api/projects/status/296i1mvgm7fht0f6?svg=true 105 | .. _win32 15 build C++17: https://ci.appveyor.com/project/ddinu/observable-2lmia 106 | 107 | .. |win64 15 build C++17| image:: https://ci.appveyor.com/api/projects/status/i948buecj8j51by0?svg=true 108 | .. _win64 15 build C++17: https://ci.appveyor.com/project/ddinu/observable-ha4xx 109 | 110 | Visual Studio 2015 builds: 111 | 112 | * |win32 14 build|_ (32 bit, C++14) 113 | * |win64 14 build|_ (64 bit, C++14) 114 | 115 | .. |win32 14 build| image:: https://ci.appveyor.com/api/projects/status/bee1g4nlh25olmct/branch/master?svg=true 116 | .. _win32 14 build: https://ci.appveyor.com/project/ddinu/observable-xwigk 117 | 118 | .. |win64 14 build| image:: https://ci.appveyor.com/api/projects/status/abi5swnpvc2nof3r/branch/master?svg=true 119 | .. _win64 14 build: https://ci.appveyor.com/project/ddinu/observable 120 | 121 | Linux (GCC, Clang) and OS X (Clang) builds: 122 | 123 | * |travis build|_ (64 bit) 124 | 125 | .. |travis build| image:: https://travis-ci.org/ddinu/observable.svg?branch=master 126 | .. _travis build: https://travis-ci.org/ddinu/observable 127 | -------------------------------------------------------------------------------- /observable/include/observable/expressions/utility.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 9 | 10 | namespace observable { inline namespace expr { namespace expr_detail { 11 | 12 | //! Check if a type is either an expression_node or an observable 13 | //! value. 14 | //! 15 | //! The static member ``value`` will be true if the provided type is either an 16 | //! observable value or an 17 | //! expression_node. 18 | //! 19 | //! \ingroup observable_detail 20 | template 21 | struct is_observable : 22 | std::integral_constant::value || 23 | is_expression_node::value> 24 | { }; 25 | 26 | //! Check if any of the provided types are observable. 27 | //! 28 | //! The static member ``value`` will be true if at least one of the provided types 29 | //! is an observable value or an expression_node. 30 | //! 31 | //! \ingroup observable_detail 32 | template 33 | struct are_any_observable; 34 | 35 | //! \cond 36 | template 37 | struct are_any_observable : 38 | std::integral_constant::value || 39 | are_any_observable::value> 40 | { }; 41 | 42 | template 43 | struct are_any_observable : is_observable 44 | { }; 45 | //! \endcond 46 | 47 | //! \cond 48 | template 49 | struct val_type_ { using type = T; }; 50 | 51 | template 52 | struct val_type_> { using type = T; }; 53 | 54 | template 55 | struct val_type_> { using type = T; }; 56 | //! \endcond 57 | 58 | //! Extract the value type from an expression_node or observable 59 | //! value. 60 | //! 61 | //! \ingroup observable_detail 62 | template 63 | struct val_type : val_type_> { }; 64 | 65 | //! Convenience typedef for extracting the value type from an expression_node or 66 | //! observable value. 67 | //! 68 | //! \see val_type 69 | //! \ingroup observable_detail 70 | template 71 | using val_type_t = typename val_type::type; 72 | 73 | //! Computes the type of the expression_node created for an expression with 74 | //! callable ``Op`` and corresponding arguments. 75 | //! 76 | //! \ingroup observable_detail 77 | template 78 | struct result_node 79 | { 80 | #if defined(__cpp_lib_invoke) && __cpp_lib_invoke && defined(_HAS_CXX17) && _HAS_CXX17 81 | using type = expression_node< 82 | std::decay_t< 83 | std::invoke_result_t< 84 | std::decay_t, val_type_t ...>>>; 85 | #else 86 | using type = expression_node< 87 | std::decay_t< 88 | std::result_of_t< 89 | std::decay_t(val_type_t ...)>>>; 90 | #endif 91 | }; 92 | 93 | //! Type of the expression_node created for an expression with callable ``Op`` and 94 | //! corresponding arguments. 95 | //! 96 | //! \ingroup observable_detail 97 | template 98 | using result_node_t = typename result_node::type; 99 | 100 | //! Create a node from a regular type. 101 | //! 102 | //! \ingroup observable_detail 103 | template 104 | inline auto make_node(T && val) 105 | { 106 | return expression_node> { std::forward(val) }; 107 | } 108 | 109 | //! Create a node from an observable value reference. 110 | //! 111 | //! \ingroup observable_detail 112 | template 113 | inline auto make_node(value & val) 114 | { 115 | return expression_node { val }; 116 | } 117 | 118 | //! Create a node from a temporary expression_node. 119 | //! 120 | //! \ingroup observable_detail 121 | template 122 | inline auto make_node(expression_node && node) 123 | { 124 | return std::move(node); 125 | } 126 | 127 | //! Create a node from an operator and an arbitrary number of arguments. 128 | //! 129 | //! \ingroup observable_detail 130 | template 131 | inline auto make_node(Op && op, Args && ... args) 132 | { 133 | return result_node_t { 134 | std::forward(op), 135 | make_node(std::forward(args)) ... 136 | }; 137 | } 138 | 139 | } } } 140 | 141 | OBSERVABLE_END_CONFIGURE_WARNINGS 142 | -------------------------------------------------------------------------------- /tests/src/expressions/expression.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace observable { inline namespace expr { namespace test { 6 | 7 | TEST_CASE("expression/expression creation", "[expression]") 8 | { 9 | SECTION("can create immediate-update expression") 10 | { 11 | auto e = expression { expression_node { 5 } }; 12 | 13 | REQUIRE(e.get() == 5); 14 | } 15 | 16 | SECTION("can create manual-update expression") 17 | { 18 | auto ev = expression_evaluator { }; 19 | auto e = expression { expression_node { 5 }, ev }; 20 | 21 | REQUIRE(e.get() == 5); 22 | } 23 | } 24 | 25 | TEST_CASE("expression/interactions with values", "[expression]") 26 | { 27 | SECTION("can get immediate-update expression's value") 28 | { 29 | auto e = expression { expression_node { 5 } }; 30 | 31 | REQUIRE(e.get() == 5); 32 | } 33 | 34 | SECTION("can get manual-update expression's value") 35 | { 36 | auto ev = expression_evaluator { }; 37 | auto e = expression { expression_node { 5 }, ev }; 38 | 39 | REQUIRE(e.get() == 5); 40 | } 41 | 42 | SECTION("can create value from expression") 43 | { 44 | auto val = value { 45 | std::make_unique>( 46 | expression_node { 5 } 47 | ) 48 | }; 49 | 50 | REQUIRE(val.get() == 5); 51 | } 52 | 53 | SECTION("expression change updates value") 54 | { 55 | auto val1 = value { 5 }; 56 | auto val2 = value { 57 | std::make_unique>( 58 | expression_node { val1 } 59 | ) 60 | }; 61 | 62 | val1 = 7; 63 | 64 | REQUIRE(val2.get() == 7); 65 | } 66 | 67 | SECTION("can convert expression to value") 68 | { 69 | auto val1 = value { 5 }; 70 | auto val2 = value { 71 | std::make_unique>( 72 | expression_node { val1 } 73 | ) 74 | }; 75 | val1 = 7; 76 | 77 | REQUIRE(val2.get() == 7); 78 | } 79 | } 80 | 81 | TEST_CASE("expression/expression updating", "[expression]") 82 | { 83 | SECTION("immediate-update expression is updated on change") 84 | { 85 | auto val = value { 5 }; 86 | auto e = expression { expression_node { val } }; 87 | val = 7; 88 | 89 | REQUIRE(e.get() == 7); 90 | } 91 | 92 | SECTION("immediate-update expression can be manually updated") 93 | { 94 | auto val = value { 5 }; 95 | auto e = expression { expression_node { val } }; 96 | val = 7; 97 | e.eval(); 98 | 99 | REQUIRE(e.get() == 7); 100 | } 101 | 102 | SECTION("manual-update expression is not updated on change") 103 | { 104 | auto ev = expression_evaluator { }; 105 | 106 | auto val = value { 5 }; 107 | auto e = expression { expression_node { val }, ev }; 108 | val = 7; 109 | 110 | REQUIRE(e.get() == 5); 111 | } 112 | 113 | SECTION("manual-update expression can be updated") 114 | { 115 | auto ev = expression_evaluator { }; 116 | 117 | auto val = value { 5 }; 118 | auto e = expression { expression_node { val }, ev }; 119 | val = 7; 120 | e.eval(); 121 | 122 | REQUIRE(e.get() == 7); 123 | } 124 | 125 | SECTION("can globally update arbitrary empty evaluator") 126 | { 127 | auto ev = expression_evaluator { }; 128 | ev.eval_all(); 129 | } 130 | 131 | SECTION("can globally update expression") 132 | { 133 | auto ev = expression_evaluator { }; 134 | 135 | auto val = value { 5 }; 136 | auto e = expression { expression_node { val }, ev }; 137 | val = 7; 138 | ev.eval_all(); 139 | 140 | REQUIRE(e.get() == 7); 141 | } 142 | 143 | SECTION("can globally update multiple expressions") 144 | { 145 | auto ev = expression_evaluator { }; 146 | 147 | auto val1 = value { 5 }; 148 | auto e1 = expression { expression_node { val1 }, ev }; 149 | 150 | auto val2 = value { 13 }; 151 | auto e2 = expression { expression_node { val2 }, ev }; 152 | 153 | val1 = 7; 154 | val2 = 17; 155 | 156 | ev.eval_all(); 157 | 158 | REQUIRE(e1.get() == 7); 159 | REQUIRE(e2.get() == 17); 160 | } 161 | } 162 | 163 | } } } 164 | -------------------------------------------------------------------------------- /observable/include/observable/observe.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 11 | 12 | namespace observable { 13 | 14 | //! Update all observable values that were associated with an updater instance. 15 | //! 16 | //! You associate an observable value with an updater instance by providing the 17 | //! updater as a first parameter to ``observe()``. 18 | //! 19 | //! \note This class can be used as-is or derived. 20 | //! 21 | //! \ingroup observable 22 | class updater : public expr::expression_evaluator 23 | { 24 | public: 25 | //! Update all observable values that have been associated with this instance. 26 | void update_all() { eval_all(); } 27 | 28 | private: 29 | using expr::expression_evaluator::eval_all; 30 | }; 31 | 32 | //! Observe changes to a single value with automatic synchronization. 33 | //! 34 | //! Returns an observable value that is kept in-sync with the provided value. 35 | //! 36 | //! \param[in] val Value to observe. 37 | //! \return An observable value that automatically mirrors the provided parameter. 38 | //! 39 | //! \ingroup observable 40 | template 41 | inline auto observe(value & val) 42 | { 43 | using value_type = std::decay_t; 44 | using expression_type = expr::expression; 45 | auto e = std::make_unique(expr::expression_node { val }); 46 | return value { std::move(e) }; 47 | } 48 | 49 | //! Observe changes to an expression tree with automatic evaluation. 50 | //! 51 | //! Returns a value that is updated whenever the provided expression tree changes. 52 | //! 53 | //! \param[in] root Expression tree to observe. 54 | //! \return An observable value that is automatically updated when the provided 55 | //! expression tree changes. 56 | //! 57 | //! \ingroup observable 58 | template 59 | inline auto observe(expr::expression_node && root) 60 | { 61 | using expression_type = expr::expression; 62 | auto e = std::make_unique(std::move(root)); 63 | return value { std::move(e) }; 64 | } 65 | 66 | //! Observe changes to a single value with manual synchronization. 67 | //! 68 | //! Returns an observable value that is synchronized with the provided value 69 | //! whenever the ``update()`` method is called on the provided \ref updater. 70 | //! 71 | //! \param[in] ud An \ref updater instance to be used for manually synchronizing 72 | //! the returned value to the provided value. 73 | //! \param[in] val A value to synchronize with the returned value. 74 | //! \return An observable value that is manually synchronized to the provided 75 | //! value. 76 | //! 77 | //! \ingroup observable 78 | template 79 | inline auto observe(UpdaterType & ud, value & val) 80 | { 81 | static_assert(std::is_base_of::value, 82 | "UpdaterType must derive from updater."); 83 | 84 | using value_type = std::decay_t; 85 | using expression_type = expr::expression; 86 | auto root = expr::expression_node { val }; 87 | auto e = std::make_unique(std::move(root), ud); 88 | return value { std::move(e) }; 89 | } 90 | 91 | //! Observe changes to an expression tree with manual synchronization. 92 | //! 93 | //! Returns an observable value that is updated when the ``update()`` method is 94 | //! called on the provided \ref updater. 95 | //! 96 | //! \note The expression tree will only be evaluated if there have been changes 97 | //! since the last update() call. 98 | //! 99 | //! \param[in] ud An \ref updater instance to be used for manually updating the 100 | //! returned value with the expression tree. 101 | //! \param[in] root An expression tree to be used for updating the returned value. 102 | //! \return An observable value that is updated from the provided expression. 103 | //! 104 | //! \ingroup observable 105 | template 106 | inline auto observe(UpdaterType & ud, expr::expression_node && root) 107 | { 108 | static_assert(std::is_base_of::value, 109 | "UpdaterType must derive from updater."); 110 | 111 | using expression_type = expr::expression; 112 | auto e = std::make_unique(std::move(root), ud); 113 | return value { std::move(e) }; 114 | } 115 | 116 | } 117 | 118 | OBSERVABLE_END_CONFIGURE_WARNINGS 119 | -------------------------------------------------------------------------------- /docs/example_tests/src/getting_started.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utility.h" 6 | 7 | using namespace std::string_literals; 8 | 9 | TEST_CASE("documentation examples/getting started", "[documentation examples]") 10 | { 11 | provide_cin cin_buf { "Jane"s }; 12 | capture_cout cout_buf; 13 | 14 | SECTION("getting started with subjects") 15 | { 16 | // BEGIN EXAMPLE CODE 17 | using namespace std; 18 | using namespace observable; 19 | 20 | // Will print "Hello {name}" each time you enter a name. 21 | // Type exit to stop. 22 | 23 | //int main() 24 | { 25 | auto name_entered = subject { }; 26 | 27 | // Subscribe to name_entered notifications. 28 | name_entered.subscribe([](auto const & name) { 29 | cout << "Hello "s 30 | << name 31 | << "!"s 32 | << endl; 33 | }); 34 | 35 | while(cin) 36 | { 37 | cout << "What's your name?"s << endl; 38 | 39 | // Read the name. 40 | auto input_name = string { }; 41 | cin >> input_name; 42 | 43 | if(input_name.empty() || input_name == "exit"s) 44 | break; 45 | 46 | // Notify all name_entered observers. 47 | name_entered.notify(input_name); 48 | } 49 | 50 | //return 0; 51 | } 52 | // END EXAMPLE CODE 53 | 54 | REQUIRE("What's your name?\nHello Jane!\nWhat's your name?\n"s == 55 | cout_buf.str()); 56 | } 57 | 58 | SECTION("getting started with values") 59 | { 60 | // BEGIN EXAMPLE CODE 61 | using namespace std; 62 | using namespace observable; 63 | 64 | // Will print "Hello {name}" each time you enter a name. 65 | // Type exit to stop. 66 | 67 | //int main() 68 | { 69 | auto name = value { }; 70 | 71 | // Recompute the greeting each time name changes and 72 | // fire change notifications. 73 | auto greeting = observe("Hello "s + name + "!"s); 74 | 75 | // Subscribe to greeting changes. 76 | greeting.subscribe([](auto const & hello) { 77 | cout << hello << endl; 78 | }); 79 | 80 | while(cin) 81 | { 82 | cout << "What's your name?"s << endl; 83 | 84 | // Read the name. 85 | auto input_name = string { }; 86 | cin >> input_name; 87 | 88 | if(input_name.empty() || input_name == "exit"s) 89 | break; 90 | 91 | // Update the name value. 92 | name = input_name; 93 | } 94 | 95 | //return 0; 96 | } 97 | // END EXAMPLE CODE 98 | 99 | REQUIRE("What's your name?\nHello Jane!\nWhat's your name?\n"s == 100 | cout_buf.str()); 101 | } 102 | 103 | SECTION("getting started with properties") 104 | { 105 | // BEGIN EXAMPLE CODE 106 | using namespace std; 107 | using namespace observable; 108 | 109 | // Will print "Hello {name}" each time you enter a name. 110 | // Type exit to stop. 111 | 112 | // Greet people using names read from stdin. 113 | class Greeter 114 | { 115 | OBSERVABLE_PROPERTIES(Greeter) 116 | 117 | public: 118 | // Current name. 119 | observable_property name; 120 | 121 | // Current greeting. 122 | observable_property greeting = observe( 123 | "Hello "s + name + "!"s 124 | ); 125 | 126 | public: 127 | // Read names from stdin until the user quits. 128 | void read_names() 129 | { 130 | while(cin) 131 | { 132 | cout << "What's your name?"s << endl; 133 | 134 | auto input_name = string { }; 135 | cin >> input_name; 136 | 137 | if(input_name.empty() || input_name == "exit"s) 138 | break; 139 | 140 | name = input_name; 141 | } 142 | } 143 | }; 144 | 145 | //int main() 146 | { 147 | Greeter greeter; 148 | 149 | // Print the greetings. 150 | greeter.greeting.subscribe([](auto const & hello) { 151 | cout << hello << endl; 152 | }); 153 | 154 | // Properties cannot be set from outside the class. The 155 | // line below will not compile: 156 | // greeter.name = input_name; 157 | 158 | greeter.read_names(); 159 | //return 0; 160 | } 161 | // END EXAMPLE CODE 162 | 163 | REQUIRE("What's your name?\nHello Jane!\nWhat's your name?\n"s == 164 | cout_buf.str()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /observable/include/observable/expressions/operators.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 8 | 9 | //! Create a unary operator. 10 | //! 11 | //! \ingroup observable_detail 12 | #define OBSERVABLE_DEFINE_UNARY_OP(OP) \ 13 | template \ 14 | inline auto operator OP (value & arg) \ 15 | noexcept(noexcept(OP arg.get())) \ 16 | -> expression_node> \ 17 | { \ 18 | return expr_detail::make_node([](auto && v) { return (OP v); }, arg); \ 19 | } \ 20 | \ 21 | template \ 22 | inline auto operator OP (expression_node && arg) \ 23 | noexcept(noexcept(OP arg.get())) \ 24 | -> expression_node> \ 25 | { \ 26 | return expr_detail::make_node([](auto && v) { return (OP v); }, \ 27 | std::move(arg)); \ 28 | } 29 | 30 | //! Create a binary operator. 31 | //! 32 | //! \ingroup observable_detail 33 | #define OBSERVABLE_DEFINE_BINARY_OP(OP) \ 34 | template \ 35 | inline auto operator OP (value & a, B && b) \ 36 | noexcept(noexcept(a.get() OP b)) \ 37 | -> std::enable_if_t::value, \ 38 | expression_node> \ 39 | { \ 40 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 41 | a, std::forward(b)); \ 42 | } \ 43 | \ 44 | template \ 45 | inline auto operator OP (A && a, value & b) \ 46 | noexcept(noexcept(a OP b.get())) \ 47 | -> std::enable_if_t::value, \ 48 | expression_node> \ 49 | { \ 50 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 51 | std::forward(a), b); \ 52 | } \ 53 | \ 54 | template \ 55 | inline auto operator OP (value & a, value & b) \ 56 | noexcept(noexcept(a.get() OP b.get())) \ 57 | -> expression_node \ 58 | { \ 59 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 60 | a, b); \ 61 | } \ 62 | \ 63 | template \ 65 | inline auto operator OP (value & a, value & b) \ 66 | noexcept(noexcept(a.get() OP b.get())) \ 67 | -> expression_node \ 68 | { \ 69 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 70 | a, b); \ 71 | } \ 72 | \ 73 | template \ 74 | inline auto operator OP (expression_node && a, B && b) \ 75 | noexcept(noexcept(a.get() OP b)) \ 76 | -> std::enable_if_t::value, \ 77 | expression_node> \ 78 | { \ 79 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 80 | std::move(a), std::forward(b)); \ 81 | } \ 82 | \ 83 | template \ 84 | inline auto operator OP (A && a, expression_node && b) \ 85 | noexcept(noexcept(a OP b.get())) \ 86 | -> std::enable_if_t::value, \ 87 | expression_node> \ 88 | { \ 89 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 90 | std::forward(a), std::move(b)); \ 91 | } \ 92 | \ 93 | template \ 94 | inline auto operator OP (expression_node && a, expression_node && b) \ 95 | noexcept(noexcept(a.get() OP b.get())) \ 96 | -> expression_node \ 97 | { \ 98 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 99 | std::move(a), std::move(b)); \ 100 | } \ 101 | \ 102 | template \ 103 | inline auto operator OP (value & a, expression_node && b) \ 104 | noexcept(noexcept(a.get() OP b.get())) \ 105 | -> expression_node \ 106 | { \ 107 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 108 | a, std::move(b)); \ 109 | } \ 110 | \ 111 | template \ 112 | inline auto operator OP (expression_node && a, value & b) \ 113 | noexcept(noexcept(a.get() OP b.get())) \ 114 | -> expression_node \ 115 | { \ 116 | return expr_detail::make_node([](auto && av, auto && bv) { return (av OP bv); }, \ 117 | std::move(a), b); \ 118 | } 119 | 120 | namespace observable { inline namespace expr { 121 | 122 | // Unary operators. 123 | 124 | OBSERVABLE_DEFINE_UNARY_OP(!) 125 | OBSERVABLE_DEFINE_UNARY_OP(~) 126 | OBSERVABLE_DEFINE_UNARY_OP(+) 127 | OBSERVABLE_DEFINE_UNARY_OP(-) 128 | 129 | // Binary operators. 130 | 131 | OBSERVABLE_DEFINE_BINARY_OP(*) 132 | OBSERVABLE_DEFINE_BINARY_OP(/) 133 | OBSERVABLE_DEFINE_BINARY_OP(%) 134 | OBSERVABLE_DEFINE_BINARY_OP(+) 135 | OBSERVABLE_DEFINE_BINARY_OP(-) 136 | OBSERVABLE_DEFINE_BINARY_OP(<<) 137 | OBSERVABLE_DEFINE_BINARY_OP(>>) 138 | OBSERVABLE_DEFINE_BINARY_OP(<) 139 | OBSERVABLE_DEFINE_BINARY_OP(<=) 140 | OBSERVABLE_DEFINE_BINARY_OP(>) 141 | OBSERVABLE_DEFINE_BINARY_OP(>=) 142 | OBSERVABLE_DEFINE_BINARY_OP(==) 143 | OBSERVABLE_DEFINE_BINARY_OP(!=) 144 | OBSERVABLE_DEFINE_BINARY_OP(&) 145 | OBSERVABLE_DEFINE_BINARY_OP(^) 146 | OBSERVABLE_DEFINE_BINARY_OP(|) 147 | OBSERVABLE_DEFINE_BINARY_OP(&&) 148 | OBSERVABLE_DEFINE_BINARY_OP(||) 149 | 150 | } } 151 | 152 | OBSERVABLE_END_CONFIGURE_WARNINGS 153 | -------------------------------------------------------------------------------- /docs/sphinx/pages/using_expressions.rst: -------------------------------------------------------------------------------- 1 | How to use expressions 2 | ====================== 3 | 4 | Observable expressions are a simple way of composing `observable values`_ and 5 | constants. 6 | 7 | .. _`observable values`: ../reference/classobservable_1_1value_3_01_value_type_00_01_equality_comparator_01_4.html 8 | 9 | Simple expressions 10 | ------------------ 11 | 12 | You can use operators, like ``+`` and ``*``, to combine observable values into 13 | arbitrary expressions. These expressions are converted to values by passing them 14 | to the `observe()`_ functions. 15 | 16 | .. _`observe()`: ../reference/group__observable.html#ga25c1181fc75df6d45c0e8da530ce8639 17 | 18 | .. note:: If the observable value's contained type does not support an operator, 19 | neither will the observable value. 20 | 21 | Here's a small example: 22 | 23 | .. code-block:: C++ 24 | 25 | #include 26 | #include 27 | 28 | int main() 29 | { 30 | auto radius = observable::value { 5 }; 31 | auto circumference = observe(2 * M_PI * radius); 32 | 33 | assert(fabs(circumference.get() - 2 * M_PI * 5) < 0.001); 34 | 35 | radius = 7; 36 | 37 | assert(fabs(circumference.get() - 2 * M_PI * 7) < 0.001); 38 | } 39 | 40 | The ``radius`` variable is an observable value. Because of that, it can be used 41 | in an expression, which, when passed to observe, produces another value: 42 | ``circumference``. 43 | 44 | The ``circumference`` value will be updated each time ``radius`` gets changed. 45 | 46 | Using updaters 47 | -------------- 48 | 49 | In the previous example, the update is automatic and immediate, but you can 50 | control when the expression is recomputed by passing an `updater`_ to 51 | `observe()`_. 52 | 53 | .. _`updater`: ../reference/classobservable_1_1updater.html 54 | 55 | Here's how using an `updater`_ works: 56 | 57 | .. code-block:: C++ 58 | 59 | #include 60 | #include 61 | 62 | int main() 63 | { 64 | auto radius = observable::value { 5 }; 65 | 66 | auto updater = observable::updater { }; 67 | auto circumference = observe(updater, 2 * M_PI * radius); 68 | 69 | assert(fabs(circumference.get() - 2 * M_PI * 5) < 0.001); 70 | 71 | radius = 7; 72 | assert(fabs(circumference.get() - 2 * M_PI * 5) < 0.001); 73 | 74 | updater.update_all(); 75 | assert(fabs(circumference.get() - 2 * M_PI * 7) < 0.001); 76 | } 77 | 78 | When you call ``update_all()``, all values returned by ``observe()`` calls, 79 | which got passed the updater instance, will be updated with the new result 80 | of the expression. 81 | 82 | By using updaters, you can have complete control over when expressions 83 | are evaluated. 84 | 85 | You can use multiple updaters at the same time. 86 | 87 | Another benefit of using updaters is that your expression will not be evaluated 88 | multiple times, because you can delay the call to ``update_all()`` until 89 | you know that all values that will be changed, have changed. 90 | 91 | Expression filters 92 | ------------------ 93 | 94 | An expression filter is a callable object that can take one or more expression 95 | nodes as parameters and returns an expression node [#]_. Basically, it's a 96 | function that can be used inside expressions. 97 | 98 | .. [#] The low-level components of an expression are called *expression nodes*. 99 | 100 | Predefined filters 101 | ++++++++++++++++++ 102 | 103 | There are a number of `predefined filters <../reference/group__observable__expressions.html>`_, 104 | that you can check out in the `reference <../reference/group__observable__expressions.html>`_. 105 | 106 | Like with operators, you can take advantage of `ADL`_ and just use the filter's 107 | unqualified name. 108 | 109 | .. _`ADL`: http://en.cppreference.com/w/cpp/language/adl 110 | 111 | .. code-block:: C++ 112 | 113 | #include 114 | #include 115 | 116 | int main() 117 | { 118 | auto radius = observable::value { 5 }; 119 | auto area = observe(M_PI * pow(radius, 2)); 120 | auto is_large = observe(select(area > 100, true, false)); 121 | 122 | assert(fabs(area.get() - M_PI * std::pow(5, 2)) < 0.001); 123 | assert(is_large.get() == false); 124 | 125 | radius = 70; 126 | 127 | assert(fabs(area.get() - M_PI * std::pow(70, 2)) < 0.001); 128 | assert(is_large.get() == true); 129 | } 130 | 131 | User defined filters 132 | ++++++++++++++++++++ 133 | 134 | You can write your own expression filters. 135 | 136 | It's pretty easy as you won't need to handle the expression nodes directly; 137 | just write a normal function taking the right values and use the 138 | `OBSERVABLE_ADAPT_FILTER <../reference/group__observable__expressions.html#ga06de81bd93a814eefde0b3ba3118d3fe>`_ 139 | macro. 140 | 141 | The predefined filters are created with the same macro. 142 | 143 | Let's take a look at an example: 144 | 145 | .. code-block:: C++ 146 | 147 | #include 148 | #include 149 | 150 | double square_(double val) { return std::pow(val, 2); } 151 | OBSERVABLE_ADAPT_FILTER(square, square_) 152 | 153 | int main() 154 | { 155 | auto radius = observable::value { 5 }; 156 | auto area = observe(M_PI * square(radius)); 157 | 158 | assert(fabs(area.get() - M_PI * std::pow(5, 2)) < 0.001); 159 | 160 | radius = 70; 161 | 162 | assert(fabs(area.get() - M_PI * std::pow(70, 2)) < 0.001); 163 | } 164 | 165 | The function that you provide to the macro will be called each time 166 | the expression is evaluated, so keep it fast. 167 | 168 | The filter will be declared in the same namespace where the macro is used. 169 | 170 | Conclusion 171 | ---------- 172 | 173 | Instead of using subscribe and callbacks, expressions are an easy way of 174 | constructing and updating values. 175 | 176 | Check out the `expression reference <../reference/group__observable__expressions.html>`_ 177 | for more details. -------------------------------------------------------------------------------- /docs/sphinx/getting-started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | The library has a few classes and a set of macros and function overloads: 5 | 6 | `subject\`_ 7 | Provides a way to notify subscribed observers when an event occurs. 8 | 9 | `value\`_ 10 | Holds value types and provides a way to notify subscribed observers when 11 | the value changes. 12 | 13 | `OBSERVABLE_PROPERTIES`_ and `observable_property`_ 14 | Values nested inside an enclosing class with setters accessible only from 15 | inside that class. 16 | 17 | `observe()`_ 18 | A group of overloaded functions that can create observable values that are 19 | updated when their argument expression changes. 20 | 21 | .. _subject\: reference/classobservable_1_1subject_3_01void_07_args_01_8_8_8_08_4.html 22 | .. _value\: reference/classobservable_1_1value_3_01_value_type_00_01_equality_comparator_01_4.html 23 | .. _OBSERVABLE_PROPERTIES: reference/group__observable.html#ga29f96693ca8b710b884b72860149fb7b 24 | .. _observable_property: reference/group__observable.html#gacca2b9245c501c1f5f71fabd516a66d3 25 | .. _observe(): reference/group__observable.html#ga25c1181fc75df6d45c0e8da530ce8639 26 | 27 | Getting started with subjects 28 | ----------------------------- 29 | 30 | Subjects are the simplest observable objects. They provide a way to get notified 31 | when some event occurs. 32 | 33 | .. code-block:: C++ 34 | 35 | #include 36 | #include 37 | #include 38 | 39 | using namespace std; 40 | using namespace observable; 41 | 42 | // Will print "Hello {name}" each time you enter a name. 43 | // Type exit to stop. 44 | 45 | int main() 46 | { 47 | auto name_entered = subject { }; 48 | 49 | // Subscribe to name_entered notifications. 50 | name_entered.subscribe([](auto const & name) { 51 | cout << "Hello "s 52 | << name 53 | << "!"s 54 | << endl; 55 | }); 56 | 57 | while(cin) 58 | { 59 | cout << "What's your name?"s << endl; 60 | 61 | // Read the name. 62 | auto input_name = string { }; 63 | cin >> input_name; 64 | 65 | if(input_name.empty() || input_name == "exit"s) 66 | break; 67 | 68 | // Notify all name_entered observers. 69 | name_entered.notify(input_name); 70 | } 71 | 72 | return 0; 73 | } 74 | 75 | Getting started with values 76 | --------------------------- 77 | 78 | Values are wrappers around value-types. You can subscribe to changes to the 79 | contained value-type. 80 | 81 | The ``observe()`` functions can be used to generate values associated with 82 | an expression. For example: ``auto result = observe((a + b) / 2.0)``. 83 | The ``result`` value will recompute the average, each time either the ``a`` 84 | or ``b`` value changes. 85 | 86 | .. code-block:: C++ 87 | 88 | #include 89 | #include 90 | #include 91 | 92 | using namespace std; 93 | using namespace observable; 94 | 95 | // Will print "Hello {name}" each time you enter a name. 96 | // Type exit to stop. 97 | 98 | int main() 99 | { 100 | auto name = value { }; 101 | 102 | // Recompute the greeting each time name changes and 103 | // fire change notifications. 104 | auto greeting = observe("Hello "s + name + "!"s); 105 | 106 | // Subscribe to greeting changes. 107 | greeting.subscribe([](auto const & hello) { 108 | cout << hello << endl; 109 | }); 110 | 111 | while(cin) 112 | { 113 | cout << "What's your name?"s << endl; 114 | 115 | // Read the name. 116 | auto input_name = string { }; 117 | cin >> input_name; 118 | 119 | if(input_name.empty() || input_name == "exit"s) 120 | break; 121 | 122 | // Update the name value. 123 | name = input_name; 124 | } 125 | 126 | return 0; 127 | } 128 | 129 | Getting started with observable properties 130 | ------------------------------------------ 131 | 132 | Properties are just values that are nested inside a class. This makes all 133 | setters inaccessible from outside the enclosing class. 134 | 135 | .. code-block:: C++ 136 | 137 | #include 138 | #include 139 | #include 140 | 141 | using namespace std; 142 | using namespace observable; 143 | 144 | // Will print "Hello {name}" each time you enter a name. 145 | // Type exit to stop. 146 | 147 | // Greet people using names read from stdin. 148 | class Greeter 149 | { 150 | OBSERVABLE_PROPERTIES(Greeter) 151 | 152 | public: 153 | // Current name. 154 | observable_property name; 155 | 156 | // Current greeting. 157 | observable_property greeting = observe( 158 | "Hello "s + name + "!"s 159 | ); 160 | 161 | public: 162 | // Read names from stdin until the user quits. 163 | void read_names() 164 | { 165 | while(cin) 166 | { 167 | cout << "What's your name?"s << endl; 168 | 169 | auto input_name = string { }; 170 | cin >> input_name; 171 | 172 | if(input_name.empty() || input_name == "exit"s) 173 | break; 174 | 175 | name = input_name; 176 | } 177 | } 178 | }; 179 | 180 | int main() 181 | { 182 | Greeter greeter; 183 | 184 | // Print the greetings. 185 | greeter.greeting.subscribe([](auto const & hello) { 186 | cout << hello << endl; 187 | }); 188 | 189 | // Properties cannot be set from outside the class. The 190 | // line below will not compile: 191 | // greeter.name = input_name; 192 | 193 | greeter.read_names(); 194 | return 0; 195 | } 196 | -------------------------------------------------------------------------------- /tests/src/expressions/math.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace { // Filters should be usable with ADL. 8 | 9 | using observable::value; 10 | using namespace Catch::literals; 11 | 12 | TEST_CASE("math filter/abs", "[math filter]") 13 | { 14 | auto a = value { 5 }; 15 | auto res = observe(abs(a)); 16 | 17 | REQUIRE(res.get() == 5); 18 | 19 | a = -7; 20 | REQUIRE(res.get() == 7); 21 | } 22 | 23 | TEST_CASE("math filter/div", "[make filter]") 24 | { 25 | auto x = value { 5 }; 26 | auto y = value { 3 }; 27 | auto res = observe(div(x, y)); 28 | 29 | REQUIRE(res.get().quot == std::div(5, 3).quot); 30 | REQUIRE(res.get().rem == std::div(5, 3).rem); 31 | 32 | x = 31; 33 | REQUIRE(res.get().quot == std::div(31, 3).quot); 34 | REQUIRE(res.get().rem == std::div(31, 3).rem); 35 | } 36 | 37 | TEST_CASE("math filter/exp", "[math filter]") 38 | { 39 | auto a = value { 5 }; 40 | auto res = observe(exp(a)); 41 | 42 | REQUIRE(res.get() == Approx { std::exp(5.0) }); 43 | 44 | a = 7; 45 | REQUIRE(res.get() == Approx { std::exp(7.0) }); 46 | } 47 | 48 | TEST_CASE("math filter/exp2", "[math filter]") 49 | { 50 | auto a = value { 5 }; 51 | auto res = observe(exp2(a)); 52 | 53 | REQUIRE(res.get() == Approx { std::exp2(5.0) }); 54 | 55 | a = 7; 56 | REQUIRE(res.get() == Approx { std::exp2(7.0) }); 57 | } 58 | 59 | TEST_CASE("math filter/log", "[math filter]") 60 | { 61 | auto a = value { 5 }; 62 | auto res = observe(log(a)); 63 | 64 | REQUIRE(res.get() == Approx { std::log(5.0) }); 65 | 66 | a = 7; 67 | REQUIRE(res.get() == Approx { std::log(7.0) }); 68 | } 69 | 70 | TEST_CASE("math filter/log10", "[math filter]") 71 | { 72 | auto a = value { 5 }; 73 | auto res = observe(log10(a)); 74 | 75 | REQUIRE(res.get() == Approx { std::log10(5.0) }); 76 | 77 | a = 7; 78 | REQUIRE(res.get() == Approx { std::log10(7.0) }); 79 | } 80 | 81 | TEST_CASE("math filter/log2", "[math filter]") 82 | { 83 | auto a = value { 5 }; 84 | auto res = observe(log2(a)); 85 | 86 | REQUIRE(res.get() == Approx { std::log2(5.0) }); 87 | 88 | a = 7; 89 | REQUIRE(res.get() == Approx { std::log2(7.0) }); 90 | } 91 | 92 | TEST_CASE("math filter/pow", "[math filter]") 93 | { 94 | auto b = value { 5 }; 95 | auto e = value { 3 }; 96 | auto res = observe(pow(b, e)); 97 | 98 | REQUIRE(res.get() == Approx { std::pow(5, 3) }); 99 | 100 | b = 7; 101 | REQUIRE(res.get() == Approx { std::pow(7, 3) }); 102 | 103 | e = 4; 104 | REQUIRE(res.get() == Approx { std::pow(7, 4) }); 105 | } 106 | 107 | TEST_CASE("math filter/sqrt", "[math filter]") 108 | { 109 | auto a = value { 5 }; 110 | auto res = observe(sqrt(a)); 111 | 112 | REQUIRE(res.get() == Approx { std::sqrt(5.0) }); 113 | 114 | a = 7; 115 | REQUIRE(res.get() == Approx { std::sqrt(7.0) }); 116 | } 117 | 118 | TEST_CASE("math filter/cbrt", "[math filter]") 119 | { 120 | auto a = value { 5 }; 121 | auto res = observe(cbrt(a)); 122 | 123 | REQUIRE(res.get() == Approx { std::cbrt(5.0) }); 124 | 125 | a = 7; 126 | REQUIRE(res.get() == Approx { std::cbrt(7.0) }); 127 | } 128 | 129 | TEST_CASE("math filter/hypot", "[math filter]") 130 | { 131 | auto x = value { 5 }; 132 | auto y = value { 7 }; 133 | auto res = observe(hypot(x, y)); 134 | 135 | REQUIRE(res.get() == Approx { std::hypot(5, 7) }); 136 | 137 | x = 10; 138 | REQUIRE(res.get() == Approx { std::hypot(10, 7) }); 139 | 140 | y = 20; 141 | REQUIRE(res.get() == Approx { std::hypot(10, 20) }); 142 | } 143 | 144 | TEST_CASE("math filter/sin", "[math filter]") 145 | { 146 | auto a = value { 5 }; 147 | auto res = observe(sin(a)); 148 | 149 | REQUIRE(res.get() == Approx { std::sin(5.0) }); 150 | 151 | a = 7; 152 | REQUIRE(res.get() == Approx { std::sin(7.0) }); 153 | } 154 | 155 | TEST_CASE("math filter/cos", "[math filter]") 156 | { 157 | auto a = value { 5 }; 158 | auto res = observe(cos(a)); 159 | 160 | REQUIRE(res.get() == Approx { std::cos(5.0) }); 161 | 162 | a = 7; 163 | REQUIRE(res.get() == Approx { std::cos(7.0) }); 164 | } 165 | 166 | TEST_CASE("math filter/tan", "[math filter]") 167 | { 168 | auto a = value { 5 }; 169 | auto res = observe(tan(a)); 170 | 171 | REQUIRE(res.get() == Approx { std::tan(5.0) }); 172 | 173 | a = 7; 174 | REQUIRE(res.get() == Approx { std::tan(7.0) }); 175 | } 176 | 177 | TEST_CASE("math filter/asin", "[math filter]") 178 | { 179 | auto a = value { 0.5 }; 180 | auto res = observe(asin(a)); 181 | 182 | REQUIRE(res.get() == Approx { std::asin(0.5) }); 183 | 184 | a = 0.8; 185 | REQUIRE(res.get() == Approx { std::asin(0.8) }); 186 | } 187 | 188 | TEST_CASE("math filter/acos", "[math filter]") 189 | { 190 | auto a = value { 0.5 }; 191 | auto res = observe(acos(a)); 192 | 193 | REQUIRE(res.get() == Approx { std::acos(0.5) }); 194 | 195 | a = 0.8; 196 | REQUIRE(res.get() == Approx { std::acos(0.8) }); 197 | } 198 | 199 | TEST_CASE("math filter/atan", "[math filter]") 200 | { 201 | auto a = value { 5 }; 202 | auto res = observe(atan(a)); 203 | 204 | REQUIRE(res.get() == Approx { std::atan(5.0) }); 205 | 206 | a = 7; 207 | REQUIRE(res.get() == Approx { std::atan(7.0) }); 208 | } 209 | 210 | TEST_CASE("math filter/atan2", "[math filter]") 211 | { 212 | auto y = value { 5 }; 213 | auto x = value { 7 }; 214 | auto res = observe(atan2(y, x)); 215 | 216 | REQUIRE(res.get() == Approx { std::atan2(5, 7) }); 217 | 218 | y = 17; 219 | REQUIRE(res.get() == Approx { std::atan2(17, 7) }); 220 | 221 | x = 10; 222 | REQUIRE(res.get() == Approx { std::atan2(17, 10) }); 223 | } 224 | 225 | TEST_CASE("math filter/ceil", "[math filter]") 226 | { 227 | auto a = value { 0.4 }; 228 | auto res = observe(ceil(a)); 229 | 230 | REQUIRE(res.get() == 1_a); 231 | 232 | a = 1.8; 233 | REQUIRE(res.get() == 2_a); 234 | } 235 | 236 | TEST_CASE("math filter/floor", "[math filter]") 237 | { 238 | auto a = value { 0.7 }; 239 | auto res = observe(floor(a)); 240 | 241 | REQUIRE(res.get() == 0_a); 242 | 243 | a = 1.8; 244 | REQUIRE(res.get() == 1_a); 245 | } 246 | 247 | TEST_CASE("math filter/trunc", "[math filter]") 248 | { 249 | auto a = value { 0.7 }; 250 | auto res = observe(trunc(a)); 251 | 252 | REQUIRE(res.get() == 0_a); 253 | 254 | a = 1.8; 255 | REQUIRE(res.get() == 1_a); 256 | } 257 | 258 | TEST_CASE("math filter/round", "[math filter]") 259 | { 260 | auto a = value { 0.4 }; 261 | auto res = observe(round(a)); 262 | 263 | REQUIRE(res.get() == 0_a); 264 | 265 | a = 1.8; 266 | REQUIRE(res.get() == 2_a); 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /observable/include/observable/subscription.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 10 | 11 | namespace observable { 12 | 13 | //! Infinite subscription that will not unsubscribe the associated observer 14 | //! when destroyed. 15 | //! 16 | //! \ingroup observable 17 | class infinite_subscription 18 | { 19 | public: 20 | //! Create a subscription with the specified unsubscribe functor. 21 | //! 22 | //! \param[in] unsubscribe Calling this functor will unsubscribe the 23 | //! associated observer. 24 | //! \note This is for internal use by subject instances. 25 | explicit infinite_subscription(std::function const & unsubscribe) : 26 | unsubscribe_ { unsubscribe } 27 | { } 28 | 29 | //! Unsubscribe the associated observer from receiving notifications. 30 | //! 31 | //! Only the first call of this method will have an effect. 32 | //! 33 | //! \note If release() has been called, this method will have no effect. 34 | void unsubscribe() 35 | { 36 | if(!called_ || !unsubscribe_ || called_->test_and_set()) 37 | return; 38 | 39 | try { 40 | if(unsubscribe_) unsubscribe_(); 41 | } catch(...) { 42 | called_->clear(); 43 | throw; 44 | } 45 | } 46 | 47 | //! Disassociate the subscription from the subscribed observer. 48 | //! 49 | //! After calling this method, calling unsubscribe() or destroying the 50 | //! subscription instance will have no effect. 51 | //! 52 | //! \return Functor taking no parameters that will perform the unsubscribe 53 | //! when called. 54 | //! For example: ``subscription.release()()`` is equivalent to 55 | //! ``subscription.unsubscribe()``. 56 | auto release() 57 | { 58 | using std::swap; 59 | decltype(unsubscribe_) unsub = []() { }; 60 | swap(unsub, unsubscribe_); 61 | 62 | return unsub; 63 | } 64 | 65 | public: 66 | //! This class is default-constructible. 67 | infinite_subscription() =default; 68 | 69 | //! This class is not copy-constructible. 70 | infinite_subscription(infinite_subscription const & ) =delete; 71 | 72 | //! This class is not copy-assignable. 73 | auto operator=(infinite_subscription const &) -> infinite_subscription & =delete; 74 | 75 | //! This class is move-constructible. 76 | infinite_subscription(infinite_subscription &&) = default; 77 | 78 | //! This class is move-assignable. 79 | auto operator=(infinite_subscription &&) -> infinite_subscription & =default; 80 | 81 | private: 82 | std::function unsubscribe_ { []() { } }; 83 | 84 | // std::call_once with a std::once_flag would have worked, but it requires 85 | // pthreads on Linux. We're using this in order not to bring in that 86 | // dependency. 87 | std::unique_ptr called_ { std::make_unique() }; 88 | }; 89 | 90 | //! Unsubscribe the associated observer when destroyed. 91 | //! 92 | //! \note All methods of this class can be safely called in parallel, from multiple 93 | //! threads. 94 | //! 95 | //! \ingroup observable 96 | class unique_subscription final : public infinite_subscription 97 | { 98 | public: 99 | //! Create an empty subscription. 100 | //! 101 | //! \note Calling unsubscribe on an empty subscription will have no effect. 102 | unique_subscription() =default; 103 | 104 | //! Create an unique subscription from an infinite_subscription. 105 | //! 106 | //! \param[in] sub An infinite subscription that will be converted to an 107 | //! unique_subscription. 108 | unique_subscription(infinite_subscription && sub) : 109 | infinite_subscription(std::move(sub)) 110 | { } 111 | 112 | //! Destructor. Will call unsubscribe(). 113 | //! 114 | //! \note If release() has been called, this will have no effect. 115 | ~unique_subscription() { unsubscribe(); } 116 | 117 | using infinite_subscription::operator=; 118 | 119 | public: 120 | //! This class is not copy-constructible. 121 | unique_subscription(unique_subscription const & ) =delete; 122 | 123 | //! This class is not copy-assignable. 124 | auto operator=(unique_subscription const &) -> unique_subscription & =delete; 125 | 126 | //! This class is move-constructible. 127 | unique_subscription(unique_subscription &&) = default; 128 | 129 | //! This class is move-assignable. 130 | auto operator=(unique_subscription &&) -> unique_subscription & =default; 131 | }; 132 | 133 | //! Unsubscribe the associated observer when the last instance of the class is 134 | //! destroyed. 135 | //! 136 | //! \note All methods of this class can be safely called in parallel, from multiple 137 | //! threads. 138 | //! 139 | //! \ingroup observable 140 | class shared_subscription final 141 | { 142 | public: 143 | //! Create a shared subscription from a temporary infinite subscription. 144 | //! 145 | //! \param subscription An infinite subscription that will be converted to a 146 | //! shared subscription. 147 | //! \note The unique subscription will be released. 148 | explicit shared_subscription(infinite_subscription && subscription) : 149 | unsubscribe_ { std::make_shared(std::move(subscription)) }, 150 | mut_ { std::make_shared() } 151 | { 152 | } 153 | 154 | //! Create an empty shared subscription. 155 | //! 156 | //! Calling unsubscribe on an empty shared subscription will have no effect. 157 | shared_subscription() noexcept =default; 158 | 159 | //! Unsubscribe the associated observer from receiving notifications. 160 | //! 161 | //! Only the first call of this method will have an effect. 162 | void unsubscribe() 163 | { 164 | if(!mut_) 165 | { 166 | assert(!unsubscribe_); 167 | return; 168 | } 169 | 170 | std::lock_guard lock { *mut_ }; 171 | unsubscribe_.reset(); 172 | } 173 | 174 | //! Return true if the subscription is not empty. 175 | explicit operator bool() const noexcept { return !!unsubscribe_; } 176 | 177 | public: 178 | //! This class is copy-constructible. 179 | shared_subscription(shared_subscription const & ) =default; 180 | 181 | //! This class is copy-assignable. 182 | auto operator=(shared_subscription const &) -> shared_subscription & =default; 183 | 184 | //! This class is move-constructible. 185 | shared_subscription(shared_subscription &&) = default; 186 | 187 | //! This class is move-assignable. 188 | auto operator=(shared_subscription &&) -> shared_subscription & =default; 189 | 190 | private: 191 | std::shared_ptr unsubscribe_; 192 | std::shared_ptr mut_; 193 | }; 194 | 195 | } 196 | 197 | OBSERVABLE_END_CONFIGURE_WARNINGS 198 | -------------------------------------------------------------------------------- /tests/src/observe.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace observable { namespace test { 6 | 7 | TEST_CASE("observe/single value evaluation", "[observe]") 8 | { 9 | SECTION("immediate update value initialization works") 10 | { 11 | auto val = value { 5 }; 12 | auto result = observe(val); 13 | 14 | REQUIRE(result.get() == 5); 15 | } 16 | 17 | SECTION("enclosed immediate update value initialization works") 18 | { 19 | struct enclosed { 20 | value val { 5 }; 21 | } enc; 22 | 23 | auto result = observe(enc.val); 24 | 25 | REQUIRE(result.get() == 5); 26 | } 27 | 28 | SECTION("single value with immediate update reflects value changes") 29 | { 30 | auto val = value { 5 }; 31 | auto result = observe(val); 32 | val = 7; 33 | 34 | REQUIRE(result.get() == 7); 35 | } 36 | 37 | SECTION("single value with manual update") 38 | { 39 | auto test_updater = updater { }; 40 | auto val = value { 5 }; 41 | auto result = observe(test_updater, val); 42 | 43 | REQUIRE(result.get() == 5); 44 | } 45 | 46 | SECTION("enclosed single value with manual update") 47 | { 48 | auto test_updater = updater { }; 49 | 50 | struct enclosed { 51 | value val { 5 }; 52 | } enc; 53 | 54 | auto result = observe(test_updater, enc.val); 55 | 56 | REQUIRE(result.get() == 5); 57 | } 58 | 59 | SECTION("single value with manual update propagates changes") 60 | { 61 | auto test_updater = updater { }; 62 | auto val = value { 5 }; 63 | auto result = observe(test_updater, val); 64 | 65 | val = 7; 66 | test_updater.update_all(); 67 | 68 | REQUIRE(result.get() == 7); 69 | } 70 | 71 | SECTION("single value with manual update does not change without calling eval()") 72 | { 73 | auto test_updater = updater { }; 74 | auto val = value { 5 }; 75 | auto result = observe(test_updater, val); 76 | val = 7; 77 | 78 | REQUIRE(result.get() == 5); 79 | } 80 | } 81 | 82 | TEST_CASE("observe/single value change notifications", "[observe]") 83 | { 84 | SECTION("single value with immediate update triggers change notification") 85 | { 86 | auto val = value { 1 }; 87 | auto result = observe(val); 88 | 89 | auto call_count = 0; 90 | result.subscribe([&]() { ++call_count; }).release(); 91 | val = 10; 92 | 93 | REQUIRE(call_count == 1); 94 | } 95 | 96 | SECTION("single value with manual update triggers change notification on eval") 97 | { 98 | auto test_updater = updater { }; 99 | auto val = value { 1 }; 100 | auto result = observe(test_updater, val); 101 | 102 | auto call_count = 0; 103 | result.subscribe([&]() { ++call_count; }).release(); 104 | val = 10; 105 | test_updater.update_all(); 106 | 107 | REQUIRE(call_count == 1); 108 | } 109 | 110 | SECTION("single value with manual update does not trigger notification without eval") 111 | { 112 | auto test_updater = updater { }; 113 | auto val = value { 1 }; 114 | auto result = observe(test_updater, val); 115 | 116 | auto call_count = 0; 117 | result.subscribe([&]() { ++call_count; }).release(); 118 | val = 10; 119 | 120 | REQUIRE(call_count == 0); 121 | } 122 | } 123 | 124 | TEST_CASE("observe/expression evaluation", "[observe]") 125 | { 126 | SECTION("chained values with immediate update are updated") 127 | { 128 | auto a = value { 1 }; 129 | auto v1 = observe(a); 130 | auto v2 = observe(v1); 131 | 132 | a = 3; 133 | 134 | REQUIRE(v2.get() == 3); 135 | } 136 | 137 | SECTION("chained expressions with immediate update are updated") 138 | { 139 | auto a = value { 1 }; 140 | auto v1 = observe(a + 2); 141 | auto v2 = observe(v1 + 2); 142 | 143 | a = 3; 144 | 145 | REQUIRE(v2.get() == 7); 146 | } 147 | 148 | SECTION("chained values with manual update are updated") 149 | { 150 | auto test_updater = updater { }; 151 | 152 | auto a = value { 1 }; 153 | auto v1 = observe(test_updater, a); 154 | auto v2 = observe(test_updater, v1); 155 | 156 | a = 3; 157 | test_updater.update_all(); 158 | 159 | REQUIRE(v2.get() == 3); 160 | } 161 | 162 | SECTION("chained expressions with manual update are updated") 163 | { 164 | auto test_updater = updater { }; 165 | 166 | auto a = value { 1 }; 167 | auto v1 = observe(test_updater, a + 2); 168 | auto v2 = observe(test_updater, v1 + 2); 169 | 170 | a = 3; 171 | test_updater.update_all(); 172 | 173 | REQUIRE(v2.get() == 7); 174 | } 175 | } 176 | 177 | TEST_CASE("observe/expression change notifications", "[observe]") 178 | { 179 | SECTION("expression with immediate update triggers change notification") 180 | { 181 | auto a = value { 1 }; 182 | auto b = value { 2 }; 183 | auto result = observe((a + b) / 2); 184 | 185 | auto call_count = 0; 186 | result.subscribe([&]() { ++call_count; }).release(); 187 | 188 | a = 10; 189 | b = 20; 190 | 191 | REQUIRE(call_count == 2); 192 | } 193 | 194 | SECTION("expression with manual update triggers change notification on eval") 195 | { 196 | auto test_updater = updater { }; 197 | auto a = value { 1 }; 198 | auto b = value { 2 }; 199 | auto result = observe(test_updater, (a + b) / 2); 200 | 201 | auto call_count = 0; 202 | result.subscribe([&]() { ++call_count; }).release(); 203 | 204 | a = 10; 205 | b = 20; 206 | test_updater.update_all(); 207 | 208 | REQUIRE(call_count == 1); 209 | } 210 | 211 | SECTION("expression with manual update does not trigger change notification without eval") 212 | { 213 | auto test_updater = updater { }; 214 | auto a = value { 1 }; 215 | auto b = value { 2 }; 216 | auto result = observe(test_updater, (a + b) / 2); 217 | 218 | auto call_count = 0; 219 | result.subscribe([&]() { ++call_count; }).release(); 220 | 221 | a = 10; 222 | b = 20; 223 | 224 | REQUIRE(call_count == 0); 225 | } 226 | } 227 | 228 | TEST_CASE("observe/expssion works after everything gets moved", "[observe]") 229 | { 230 | auto a = value { }; 231 | auto b = value { }; 232 | auto r = value { }; 233 | 234 | { 235 | auto a1 = value { 2 }; 236 | auto b1 = value { 3 }; 237 | auto r1 = observe((a1 + b1) / 2.0); 238 | 239 | a = std::move(a1); 240 | b = std::move(b1); 241 | r = std::move(r1); 242 | } 243 | 244 | a = 10; 245 | b = 30; 246 | 247 | REQUIRE(r.get() == Approx { (10 + 30) / 2.0 }); 248 | } 249 | 250 | } } 251 | -------------------------------------------------------------------------------- /tests/src/subject.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace observable { namespace test { 12 | 13 | void dummy() { } 14 | void dummy_args(int, float) { } 15 | 16 | using namespace std::chrono_literals; 17 | 18 | TEST_CASE("subject/subjects are default-constructible", "[subject]") 19 | { 20 | subject { }; 21 | REQUIRE(std::is_default_constructible>::value); 22 | } 23 | 24 | TEST_CASE("subject/can subscribe to subject", "[subject]") 25 | { 26 | auto s1 = subject { }; 27 | s1.subscribe(dummy); 28 | s1.subscribe([=]() { }); 29 | s1.subscribe([=]() mutable { }); 30 | 31 | auto s2 = subject { }; 32 | s2.subscribe(dummy_args); 33 | s2.subscribe([=](int, float) { }); 34 | s2.subscribe([=](int, float) mutable { }); 35 | s2.subscribe([=](auto, auto) { }); 36 | s2.subscribe([=](auto, auto) mutable { }); 37 | } 38 | 39 | TEST_CASE("subject/can notify subject with no subscribed observers", "[subject]") 40 | { 41 | auto s = subject { }; 42 | s.notify(); 43 | } 44 | 45 | TEST_CASE("subject/observers are called", "[subject]") 46 | { 47 | SECTION("observers are called") 48 | { 49 | auto s = subject { }; 50 | 51 | auto call_count = 0; 52 | s.subscribe([&]() { ++call_count; }).release(); 53 | s.subscribe([&]() { ++call_count; }).release(); 54 | s.notify(); 55 | 56 | REQUIRE(call_count == 2); 57 | } 58 | 59 | SECTION("observer with const reference parameters is called") 60 | { 61 | auto s = subject { }; 62 | auto call_count = 0; 63 | 64 | s.subscribe([&](int const &) { ++call_count; }).release(); 65 | s.notify(5); 66 | 67 | REQUIRE(call_count == 1); 68 | } 69 | 70 | SECTION("subject with const reference parameter calls observer") 71 | { 72 | auto s = subject { }; 73 | auto call_count = 0; 74 | 75 | s.subscribe([&](int) { ++call_count; }).release(); 76 | s.notify(5); 77 | 78 | REQUIRE(call_count == 1); 79 | } 80 | } 81 | 82 | TEST_CASE("subject/observer is not called after unsubscribing", "[subject]") 83 | { 84 | auto s = subject { }; 85 | auto call_count = 0; 86 | 87 | auto sub = s.subscribe([&]() { ++call_count; }); 88 | sub.unsubscribe(); 89 | s.notify(); 90 | 91 | REQUIRE(call_count == 0); 92 | } 93 | 94 | TEST_CASE("subject/copying", "[subject]") 95 | { 96 | SECTION("subjects are not copy-constructible") 97 | { 98 | REQUIRE_FALSE(std::is_copy_constructible>::value); 99 | } 100 | 101 | SECTION("subjects are not copy-assignable") 102 | { 103 | REQUIRE_FALSE(std::is_copy_assignable>::value); 104 | } 105 | } 106 | 107 | TEST_CASE("subject/moving", "[subject]") 108 | { 109 | SECTION("subjects are nothrow move-constructible") 110 | { 111 | REQUIRE(std::is_nothrow_move_constructible>::value); 112 | } 113 | 114 | SECTION("subjects are nothrow move-assignable") 115 | { 116 | REQUIRE(std::is_nothrow_move_assignable>::value); 117 | } 118 | 119 | SECTION("moved subject keeps subscribed observers") 120 | { 121 | auto s1 = subject { }; 122 | auto call_count = 0; 123 | 124 | s1.subscribe([&]() { ++call_count; }).release(); 125 | auto s2 = std::move(s1); 126 | s2.notify(); 127 | 128 | REQUIRE(call_count == 1); 129 | } 130 | } 131 | 132 | TEST_CASE("subject/subjects are reentrant", "[subject]") 133 | { 134 | SECTION("observer added from running notify is called on second notification") 135 | { 136 | auto s = subject { }; 137 | auto call_count = 0; 138 | 139 | auto sub = s.subscribe([&]() { 140 | ++call_count; 141 | s.subscribe([&]() { ++call_count; }).release(); 142 | }); 143 | 144 | s.notify(); 145 | REQUIRE(call_count == 1); 146 | 147 | s.notify(); 148 | REQUIRE(call_count == 3); 149 | } 150 | 151 | SECTION("can unsubscribe while notification is running") 152 | { 153 | auto s = subject { }; 154 | auto call_count = 0; 155 | 156 | auto sub = unique_subscription { }; 157 | sub = s.subscribe([&]() { ++call_count; sub.unsubscribe(); }); 158 | 159 | s.notify(); 160 | s.notify(); 161 | 162 | REQUIRE(call_count == 1); 163 | } 164 | } 165 | 166 | TEST_CASE("subject/observers run on the thread that calls notify", "[subject]") 167 | { 168 | auto s = subject { }; 169 | auto other_id = std::thread::id { }; 170 | 171 | s.subscribe([&]() { other_id = std::this_thread::get_id(); }); 172 | std::thread { [&]() { s.notify(); } }.join(); 173 | 174 | REQUIRE(std::this_thread::get_id() != other_id); 175 | } 176 | 177 | TEST_CASE("subject/observer added during notification is not called", "[subject]") 178 | { 179 | auto s = subject { }; 180 | std::atomic_int old_call_count { 0 }; 181 | std::atomic_int new_call_count { 0 }; 182 | 183 | for(auto i = 0; i < 10; ++i) 184 | s.subscribe([&]() { 185 | ++old_call_count; 186 | std::this_thread::sleep_for(5ms); 187 | }).release(); 188 | 189 | auto t = std::thread { [&]() { s.notify(); } }; 190 | 191 | for(auto i = 0; i < 100 && old_call_count == 0; ++i) 192 | std::this_thread::sleep_for(1ms); 193 | 194 | for(auto i = 0; i < 10; ++i) 195 | s.subscribe([&]() { ++new_call_count; }).release(); 196 | 197 | t.join(); 198 | 199 | REQUIRE(old_call_count == 10); 200 | REQUIRE(new_call_count == 0); 201 | } 202 | 203 | TEST_CASE("subject/can use subject enclosed in class", "[subject]") 204 | { 205 | struct Foo 206 | { 207 | subject test; 208 | 209 | void notify_test() { test.notify(); } 210 | } foo; 211 | 212 | auto called = false; 213 | foo.test.subscribe([&]() { called = true; }).release(); 214 | foo.notify_test(); 215 | 216 | REQUIRE(called); 217 | } 218 | 219 | TEST_CASE("subject/can subscribe and immediately call observer", "[subject]") 220 | { 221 | auto s = subject { }; 222 | 223 | auto call_count = 0; 224 | auto sub = s.subscribe_and_call([&]() { ++call_count; }); 225 | 226 | REQUIRE(call_count == 1); 227 | } 228 | 229 | TEST_CASE("subject/immediately called observer receives arguments", "[subject]") 230 | { 231 | auto s = subject { }; 232 | 233 | auto arg = 0; 234 | auto sub = s.subscribe_and_call([&](int v) { arg = v; }, 7); 235 | 236 | REQUIRE(arg == 7); 237 | } 238 | 239 | TEST_CASE("subject/empty", "[subject]") 240 | { 241 | SECTION("empty returns true for subject with no subscribers") 242 | { 243 | auto const s = subject { }; 244 | 245 | REQUIRE(s.empty()); 246 | } 247 | 248 | SECTION("empty returns false for subject with subscribers") 249 | { 250 | auto s = subject { }; 251 | auto const sub = s.subscribe([]() { }); 252 | 253 | REQUIRE_FALSE(s.empty()); 254 | } 255 | } 256 | 257 | } } 258 | -------------------------------------------------------------------------------- /tests/src/expressions/filters.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace { // Filters should be usable with ADL. 8 | 9 | using observable::value; 10 | 11 | auto test_filter_(int a, int b) { return a + b; } 12 | OBSERVABLE_ADAPT_FILTER(test_filter, test_filter_) 13 | 14 | TEST_CASE("filter/adapter_filter", "[filter]") 15 | { 16 | SECTION("adapter_filter computes initial value") 17 | { 18 | auto val = value { 5 }; 19 | auto res = observe(test_filter(val, 2)); 20 | 21 | REQUIRE(res.get() == 5 + 2); 22 | } 23 | 24 | SECTION("adapted_filter recomputes value") 25 | { 26 | auto val = value { 5 }; 27 | auto res = observe(test_filter(val, 2)); 28 | 29 | val = 7; 30 | 31 | REQUIRE(res.get() == (7 + 2)); 32 | } 33 | 34 | SECTION("adapted_filter can take expression parameters") 35 | { 36 | auto a = value { 1 }; 37 | auto b = value { 2 }; 38 | auto c = value { 3 }; 39 | 40 | auto res = observe(test_filter(a + b, b + c)); 41 | 42 | REQUIRE(res.get() == 1 + 2 + 2 + 3); 43 | 44 | a = 10; 45 | b = 20; 46 | c = 30; 47 | 48 | REQUIRE(res.get() == 10 + 20 + 20 + 30); 49 | } 50 | 51 | SECTION("adapted_filter can participate in expression") 52 | { 53 | auto a = value { 1 }; 54 | auto b = value { 2 }; 55 | auto c = value { 3 }; 56 | 57 | auto res = observe(a + test_filter(b, c) * 20); 58 | 59 | REQUIRE(res.get() == 1 + (2 + 3) * 20); 60 | 61 | a = 10; 62 | b = 20; 63 | c = 30; 64 | 65 | REQUIRE(res.get() == 10 + (20 + 30) * 20); 66 | } 67 | } 68 | 69 | template 70 | T test_filter_template_(int a) { return static_cast(a) / 2; } 71 | OBSERVABLE_ADAPT_FILTER_TEMPLATE(test_filter_template, test_filter_template_) 72 | 73 | TEST_CASE("filter/adapter_filter_template", "[filter]") 74 | { 75 | SECTION("adapter_filter_template computes initial value") 76 | { 77 | auto val = value { 5 }; 78 | auto res = observe(test_filter_template(val)); 79 | 80 | REQUIRE(res.get() == 2.5f); 81 | } 82 | 83 | SECTION("adapter_filter_template recomputes value") 84 | { 85 | auto val = value { 5 }; 86 | auto res = observe(test_filter_template(val)); 87 | 88 | val = 7; 89 | 90 | REQUIRE(res.get() == 3.5f); 91 | } 92 | 93 | SECTION("adapter_filter_template can take expression parameters") 94 | { 95 | auto a = value { 1 }; 96 | auto b = value { 2 }; 97 | 98 | auto res = observe(test_filter_template(a + b)); 99 | 100 | REQUIRE(res.get() == static_cast(1 + 2) / 2); 101 | 102 | a = 2; 103 | b = 3; 104 | 105 | REQUIRE(res.get() == static_cast(2 + 3) / 2); 106 | } 107 | 108 | SECTION("adapter_filter_template can participate in expression") 109 | { 110 | auto a = value { 1 }; 111 | auto b = value { 3 }; 112 | 113 | auto res = observe(a + test_filter_template(b) * 20); 114 | 115 | REQUIRE(res.get() == 1 + (3.0f / 2) * 20); 116 | 117 | a = 2; 118 | b = 3; 119 | 120 | REQUIRE(res.get() == 2 + (3.0f / 2) * 20); 121 | } 122 | } 123 | 124 | TEST_CASE("filter/select", "[filter]") 125 | { 126 | auto p = value { true }; 127 | auto a = value { 1 }; 128 | auto b = value { 2 }; 129 | 130 | auto res = observe(select(p, a, b)); 131 | 132 | REQUIRE(res.get() == 1); 133 | 134 | a = 10; 135 | REQUIRE(res.get() == 10); 136 | 137 | b = 20; 138 | REQUIRE(res.get() == 10); 139 | 140 | p = false; 141 | REQUIRE(res.get() == 20); 142 | 143 | b = 200; 144 | REQUIRE(res.get() == 200); 145 | } 146 | 147 | TEST_CASE("filter/min", "[filter]") 148 | { 149 | auto a = value { 1 }; 150 | auto b = value { 2 }; 151 | auto c = value { 3 }; 152 | 153 | auto res = observe(min(a, b, c)); 154 | 155 | REQUIRE(res.get() == 1); 156 | 157 | a = 50; 158 | b = 30; 159 | c = 40; 160 | 161 | REQUIRE(res.get() == 30); 162 | } 163 | 164 | TEST_CASE("filter/max", "[filter]") 165 | { 166 | auto a = value { 1 }; 167 | auto b = value { 2 }; 168 | auto c = value { 3 }; 169 | 170 | auto res = observe(max(a, b, c)); 171 | 172 | REQUIRE(res.get() == 3); 173 | 174 | a = 50; 175 | b = 30; 176 | c = 40; 177 | 178 | REQUIRE(res.get() == 50); 179 | } 180 | 181 | TEST_CASE("filter/mean", "[filter]") 182 | { 183 | auto a = value { 1 }; 184 | auto b = value { 2 }; 185 | auto c = value { 3 }; 186 | 187 | auto res = observe(mean(a, b, c)); 188 | 189 | REQUIRE(res.get() == (1 + 2 + 3) / 3.0); 190 | 191 | a = 10; 192 | b = 20; 193 | c = 30; 194 | 195 | REQUIRE(res.get() == (10 + 20 + 30) / 3.0); 196 | } 197 | 198 | TEST_CASE("filter/clamp", "[filter]") 199 | { 200 | auto val = value { 2 }; 201 | auto low = value { 1 }; 202 | auto high = value { 3 }; 203 | 204 | auto res = observe(clamp(val, low, high)); 205 | 206 | REQUIRE(res.get() == 2); 207 | 208 | val = 30; 209 | REQUIRE(res.get() == 3); 210 | 211 | high = 40; 212 | REQUIRE(res.get() == 30); 213 | 214 | low = 35; 215 | REQUIRE(res.get() == 35); 216 | } 217 | 218 | TEST_CASE("filter/zip", "[filter]") 219 | { 220 | auto a = value { 1 }; 221 | auto b = value { 2 }; 222 | auto c = value { 3 }; 223 | 224 | auto res = observe(zip(a, b, c)); 225 | 226 | using std::get; 227 | 228 | REQUIRE(get<0>(res.get()) == 1); 229 | REQUIRE(get<1>(res.get()) == 2); 230 | REQUIRE(get<2>(res.get()) == 3); 231 | 232 | a = 10; 233 | b = 20; 234 | c = 30; 235 | 236 | REQUIRE(get<0>(res.get()) == 10); 237 | REQUIRE(get<1>(res.get()) == 20); 238 | REQUIRE(get<2>(res.get()) == 30); 239 | } 240 | 241 | TEST_CASE("filter/construct", "[filter]") 242 | { 243 | struct mock 244 | { 245 | int ma { 0 }; 246 | int mb { 0 }; 247 | 248 | mock() =default; 249 | 250 | mock(int a, int b) : ma { a }, mb { b } 251 | { } 252 | }; 253 | 254 | auto a = value { 5 }; 255 | auto res = observe(observable::construct(a, 5)); 256 | 257 | REQUIRE(res.get().ma == 5); 258 | REQUIRE(res.get().mb == 5); 259 | 260 | a = 7; 261 | 262 | REQUIRE(res.get().ma == 7); 263 | REQUIRE(res.get().mb == 5); 264 | } 265 | 266 | TEST_CASE("filter/static_expr_cast", "[filter]") 267 | { 268 | struct mock 269 | { 270 | mock() =default; 271 | 272 | explicit mock(int a) : a_ { a } 273 | { } 274 | 275 | explicit operator int() { return a_; } 276 | 277 | private: 278 | int a_ { 0 }; 279 | }; 280 | 281 | auto a = value { mock { 5 } }; 282 | auto res = observe(observable::static_expr_cast(a)); 283 | 284 | REQUIRE(res.get() == 5); 285 | 286 | a = mock { 7 }; 287 | 288 | REQUIRE(res.get() == 7); 289 | } 290 | 291 | TEST_CASE("filter/reinterpret_expr_cast", "[filter]") 292 | { 293 | auto v1 = 5; 294 | auto s = value { reinterpret_cast(&v1) }; 295 | auto res = observe(observable::reinterpret_expr_cast(s)); 296 | 297 | REQUIRE(*res.get() == 5); 298 | 299 | auto v2 = 7; 300 | s = reinterpret_cast(&v2); 301 | 302 | REQUIRE(*res.get() == 7); 303 | } 304 | 305 | } 306 | -------------------------------------------------------------------------------- /observable/include/observable/detail/collection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include 6 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 7 | 8 | namespace observable { namespace detail { 9 | 10 | //! Thread-safe collection that can store arbitrary items and apply a functor over 11 | //! them. 12 | //! 13 | //! All methods of the collection can be safely called in parallel, from multiple 14 | //! threads. 15 | //! 16 | //! \warning The order of elements inside the collection is unspecified. 17 | //! 18 | //! \tparam ValueType Type of the elements that will be stored inside the 19 | //! collection. This type must be at least move constructible. 20 | //! \ingroup observable_detail 21 | template 22 | class collection final 23 | { 24 | public: 25 | //! Identifier for an element that has been inserted. You can use this id to 26 | //! remove a previously inserted element. 27 | using id = std::size_t; 28 | 29 | //! Create an empty collection. 30 | collection() noexcept = default; 31 | 32 | //! Insert a new element into the collection. 33 | //! 34 | //! \param[in] element The object to be inserted. 35 | //! \tparam ValueType_ Type of the inserted element. Must be convertible to 36 | //! the collection's ValueType. 37 | //! 38 | //! \return An \ref id that can be used to remove the inserted element. 39 | //! This \ref id is stable; you can use it after modifying the 40 | //! collection. 41 | //! 42 | //! \note Any apply() call running concurrently with an insert() that has 43 | //! already called its functor for at least one element, is guaranteed 44 | //! to not call the functor for this newly inserted element. 45 | template 46 | auto insert(ValueType_ && element) 47 | { 48 | auto const i = ++last_id_; 49 | auto n = std::make_unique(); 50 | n->node_id = i; 51 | n->element = std::forward(element); 52 | 53 | { 54 | auto const block_gc = gc_blocker { this }; 55 | 56 | n->next = head_.load(); 57 | while(!head_.compare_exchange_weak(n->next, n.get())) 58 | ; 59 | 60 | n.release(); 61 | } 62 | 63 | gc(); 64 | return i; 65 | } 66 | 67 | //! Remove a previously inserted element from the collection. 68 | //! 69 | //! If no element with the provided \ref id exists, this method does nothing. 70 | //! 71 | //! \param[in] element_id Id of the element to remove. This is returned by 72 | //! insert. 73 | //! 74 | //! \return True if an element of the collection was removed, false if no 75 | //! element has been removed. 76 | //! 77 | //! \note Any apply() call running concurrently with a remove() call that has 78 | //! not already called its functor with the removed element, is 79 | //! guaranteed to not call the functor with the removed element. 80 | auto remove(id const & element_id) noexcept 81 | { 82 | auto deleted = false; 83 | { 84 | auto const block_gc = gc_blocker { this }; 85 | 86 | for(auto n = head_.load(); n; n = n->next) 87 | { 88 | if(n->node_id != element_id) 89 | continue; 90 | 91 | deleted = !n->deleted.exchange(true); 92 | break; 93 | } 94 | } 95 | 96 | gc(); 97 | return deleted; 98 | } 99 | 100 | //! Apply a unary functor over all elements of the collection. 101 | //! 102 | //! The functor will be called multiple times, with each element, in an 103 | //! unspecified order. 104 | //! 105 | //! \note This method is reentrant; you can call insert() and remove() on the 106 | //! collection from inside the functor. 107 | //! 108 | //! \note It is well defined and supported to remove() the element passed 109 | //! to the functor, even before the functor returns. 110 | //! 111 | //! \param[in] fun A functor that will be called with each element of the 112 | //! collection. The functor must be assignable to a 113 | //! ``std::function``. 114 | //! 115 | //! \tparam UnaryFunctor Type of the ``fun`` parameter. 116 | template 117 | void apply(UnaryFunctor && fun) const 118 | noexcept(noexcept(fun(std::declval()))) 119 | { 120 | auto const block_gc = gc_blocker { this }; 121 | 122 | for(auto n = head_.load(); n; n = n->next) 123 | { 124 | if(n->deleted.load()) 125 | continue; 126 | 127 | fun(n->element); 128 | } 129 | } 130 | 131 | //! Return true if the collection has no elements. 132 | auto empty() const noexcept 133 | { 134 | auto const block_gc = gc_blocker { this }; 135 | auto const h = head_.load(); 136 | return !h || h->deleted; 137 | } 138 | 139 | //! Destructor. 140 | ~collection() noexcept 141 | { 142 | for(auto n = head_.load(); n; n = n->next) 143 | n->deleted.store(true); 144 | 145 | gc(); 146 | } 147 | 148 | public: 149 | //! Collections are not copy-constructible. 150 | collection(collection const &) =delete; 151 | 152 | //! Collections are not copy-assignable. 153 | auto operator=(collection const &) -> collection & =delete; 154 | 155 | //! Collections are move-constructible. 156 | collection(collection &&) =delete; 157 | 158 | //! Collections are move-assignable. 159 | auto operator=(collection &&) -> collection & =delete; 160 | 161 | private: 162 | //! Delete any nodes marked as deleted. 163 | void gc() noexcept 164 | { 165 | if(block_gc_.load() > 0 || gc_active_.exchange(true)) 166 | return; 167 | 168 | auto head = head_.load(); 169 | for(auto p = &head; *p && block_gc_ == 0;) 170 | { 171 | if(!(*p)->deleted.load()) 172 | { 173 | p = &((*p)->next); 174 | continue; 175 | } 176 | 177 | auto d = *p; 178 | *p = (*p)->next; 179 | delete d; 180 | 181 | if(p == &head) 182 | head_.store(*p); 183 | } 184 | 185 | gc_active_.store(false); 186 | } 187 | 188 | //! Block the gc() method from running for the duration of an instance's 189 | //! lifetime. 190 | struct gc_blocker 191 | { 192 | explicit gc_blocker(collection const * c) noexcept : 193 | collection_{ c } 194 | { 195 | ++collection_->block_gc_; 196 | while(collection_->gc_active_.load()) 197 | ; 198 | } 199 | 200 | ~gc_blocker() noexcept 201 | { 202 | --collection_->block_gc_; 203 | } 204 | 205 | gc_blocker() =delete; 206 | gc_blocker(gc_blocker const &) =delete; 207 | auto operator=(gc_blocker const &) -> gc_blocker & =delete; 208 | gc_blocker(gc_blocker &&) =default; 209 | auto operator=(gc_blocker &&) -> gc_blocker & =default; 210 | 211 | private: 212 | collection const * collection_; 213 | }; 214 | 215 | //! Node data. 216 | struct node 217 | { 218 | node * next { nullptr }; 219 | ValueType element; 220 | std::atomic deleted { false }; 221 | id node_id; 222 | }; 223 | 224 | private: 225 | std::atomic head_ { nullptr }; 226 | mutable std::atomic block_gc_ { 0 }; 227 | std::atomic gc_active_ { false }; 228 | std::atomic last_id_ { 0 }; 229 | }; 230 | 231 | } } 232 | 233 | OBSERVABLE_END_CONFIGURE_WARNINGS 234 | -------------------------------------------------------------------------------- /observable/include/observable/expressions/expression.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 15 | 16 | namespace observable { inline namespace expr { 17 | 18 | //! Expression evaluators can be used to manually evaluate multiple expressions at 19 | //! the same time. 20 | //! 21 | //! You can use this type as-is or extend it. 22 | //! 23 | //! \ingroup observable_detail 24 | class expression_evaluator 25 | { 26 | public: 27 | //! Evaluate all expressions associated with this instance. 28 | //! 29 | //! \note This method can be safely called in parallel, from multiple threads. 30 | void eval_all() const 31 | { 32 | std::lock_guard const lock { data_->mutex }; 33 | for(auto && p : data_->funs) 34 | p.second(); 35 | } 36 | 37 | private: 38 | // Expressions are inserted and removed in the order in which they are 39 | // created. Evaluating them in this order guarantees that the evaluation 40 | // order is correct (insert is called from the constructor, nothing can 41 | // have a dependency on the inserted expression yet). 42 | 43 | using id = void const *; 44 | 45 | //! Register a new expression to be evaluated by this evaluator. 46 | //! 47 | //! \return Instance id that can be used to unregister the expression. 48 | //! \note This method can be safely called in parallel, from multiple threads. 49 | template 50 | auto insert(ExpressionType * expr) 51 | { 52 | assert(expr); 53 | std::lock_guard const lock { data_->mutex }; 54 | 55 | data_->funs.emplace_back(expr, [=]() { expr->eval(); }); 56 | return id { expr }; 57 | } 58 | 59 | //! Unregister a previously registered expression. 60 | //! 61 | //! \param[in] instance_id Id that has been returned by `insert()`. 62 | //! \note This method can be safely called in parallel, from multiple threads. 63 | void remove(id instance_id) 64 | { 65 | std::lock_guard const lock { data_->mutex }; 66 | 67 | auto const it = find_if(begin(data_->funs), 68 | end(data_->funs), 69 | [&](auto && p) { return p.first == instance_id; }); 70 | assert(it != end(data_->funs)); 71 | if(it != end(data_->funs)) 72 | data_->funs.erase(it); 73 | } 74 | 75 | private: 76 | struct data { 77 | std::deque>> funs; 78 | std::mutex mutex; 79 | }; 80 | 81 | std::shared_ptr data_ { std::make_shared() }; 82 | 83 | template 84 | friend class expression; 85 | }; 86 | 87 | //! Expressions manage expression tree evaluation and results. 88 | //! 89 | //! Expressions are also value updaters, so they can be used for updating a 90 | //! value when an expression tree changes. 91 | //! 92 | //! \tparam ValueType The expression's result value type. This is what get() 93 | //! returns. 94 | //! \tparam EvaluatorType An instance of expression_evaluator, or a type derived 95 | //! from it. 96 | //! \warning None of the methods in this class can be safely called concurrently. 97 | //! 98 | //! \ingroup observable_detail 99 | template 100 | class expression : public value_updater 101 | { 102 | static_assert(std::is_base_of::value, 103 | "EvaluatorType needs to be derived from expression_evaluator."); 104 | 105 | public: 106 | //! Create a new expression from the root node of an expression tree. 107 | //! 108 | //! \param[in] root Expression tree root node. 109 | //! \param[in] evaluator Expression evaluator to be used for globally updating 110 | //! the expression. 111 | expression(expression_node && root, 112 | EvaluatorType const & evaluator) : 113 | root_ { std::move(root) }, 114 | evaluator_ { evaluator } 115 | { 116 | expression_id_ = evaluator_.insert(this); 117 | } 118 | 119 | //! Evaluate the expression. This will ensure that the expression's result 120 | //! is up-to-date. 121 | void eval() 122 | { 123 | root_.eval(); 124 | value_notifier_(root_.get()); 125 | } 126 | 127 | //! Retrieve the expression's result. 128 | //! 129 | //! \warning If eval() has not been called, the result might be stale. 130 | virtual auto get() const -> ValueType override { return root_.get(); } 131 | 132 | virtual void set_value_notifier(std::function const & notifier) override 133 | { 134 | value_notifier_ = notifier; 135 | } 136 | 137 | //! Destructor. 138 | virtual ~expression() override { evaluator_.remove(expression_id_); } 139 | 140 | public: 141 | //! Expressions are default-constructible. 142 | expression() =default; 143 | 144 | //! Expressions are not copy-constructible. 145 | expression(expression const &) =delete; 146 | 147 | //! Expressions are not copy-assignable. 148 | auto operator=(expression const &) -> expression & =delete; 149 | 150 | //! Expressions are move-constructible. 151 | expression(expression &&) =default; 152 | 153 | //! Expressions are move-assignable. 154 | auto operator=(expression &&) -> expression & =default; 155 | 156 | protected: 157 | //! Return the expression tree's root node. 158 | auto root_node() -> expression_node & { return root_; } 159 | 160 | private: 161 | expression_node root_; 162 | EvaluatorType evaluator_; 163 | typename EvaluatorType::id expression_id_; 164 | std::function value_notifier_ { [](auto &&) { } }; 165 | }; 166 | 167 | //! Evaluator used for expressions that are updated immediately, whenever an 168 | //! expression node changes. 169 | //! 170 | //! Expressions using this evaluator do not need manual updates. Manual update 171 | //! calls will not do anything. 172 | //! 173 | //! \ingroup observable_detail 174 | struct immediate_evaluator final : expression_evaluator { }; 175 | 176 | //! \cond 177 | inline auto get_dummy_evaluator_() 178 | { 179 | static expression_evaluator ev; 180 | return ev; 181 | } 182 | //! \endcond 183 | 184 | //! Specialized expression that is updated immediately, whenever an expression 185 | //! node changes. 186 | //! 187 | //! \see expression 188 | //! \ingroup observable_detail 189 | template 190 | class expression : 191 | public expression 192 | { 193 | public: 194 | //! Create a new expression from the root node of an expression tree. 195 | //! 196 | //! \param[in] root Expression tree root node. 197 | explicit expression(expression_node && root) : 198 | expression(std::move(root), 199 | get_dummy_evaluator_()) 200 | { 201 | sub = this->root_node().subscribe([&]() { this->eval(); }); 202 | } 203 | 204 | public: 205 | //! Expressions are default-constructible. 206 | expression() =default; 207 | 208 | //! Expressions are not copy-constructible. 209 | expression(expression const &) =delete; 210 | 211 | //! Expressions are not copy-assignable. 212 | auto operator=(expression const &) -> expression & =delete; 213 | 214 | //! Expressions are move-constructible. 215 | expression(expression &&) =default; 216 | 217 | //! Expressions are move-assignable. 218 | auto operator=(expression &&) -> expression & =default; 219 | 220 | private: 221 | unique_subscription sub; 222 | }; 223 | 224 | } } 225 | 226 | OBSERVABLE_END_CONFIGURE_WARNINGS 227 | -------------------------------------------------------------------------------- /tests/src/expressions/tree.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace observable { inline namespace expr { namespace test { 6 | 7 | TEST_CASE("expression tree/basic node evaluation", "[expression tree]") 8 | { 9 | SECTION("constant node can be evaluated") 10 | { 11 | auto node = expression_node { 5 }; 12 | node.eval(); 13 | 14 | REQUIRE(node.get() == 5); 15 | } 16 | 17 | SECTION("value node can be evaluated") 18 | { 19 | auto val = value { 5 }; 20 | auto node = expression_node { val }; 21 | node.eval(); 22 | 23 | REQUIRE(node.get() == 5); 24 | } 25 | 26 | SECTION("change is reflected in value node") 27 | { 28 | auto val = value { 5 }; 29 | auto node = expression_node { val }; 30 | val = 7; 31 | node.eval(); 32 | 33 | REQUIRE(node.get() == 7); 34 | } 35 | 36 | SECTION("change is not reflected in value node without calling eval()") 37 | { 38 | auto val = value { 5 }; 39 | auto node = expression_node { val }; 40 | val = 7; 41 | 42 | REQUIRE(node.get() == 5); 43 | } 44 | 45 | SECTION("unary node can be evaluated") 46 | { 47 | auto node = expression_node { 48 | [](auto v) { return v * 2; }, 49 | expression_node { 5 } 50 | }; 51 | node.eval(); 52 | 53 | REQUIRE(node.get() == 5 * 2); 54 | } 55 | 56 | SECTION("change is reflected in unary node") 57 | { 58 | auto val = value { 5 }; 59 | auto node = expression_node { 60 | [](auto v) { return v * 2; }, 61 | expression_node { val } 62 | }; 63 | val = 7; 64 | node.eval(); 65 | 66 | REQUIRE(node.get() == 7 * 2); 67 | } 68 | 69 | SECTION("binary node can be evaluated") 70 | { 71 | auto node = expression_node { 72 | [](auto a, auto b) { return a + b; }, 73 | expression_node { 5 }, 74 | expression_node { 7 } 75 | }; 76 | node.eval(); 77 | 78 | REQUIRE(node.get() == 5 + 7); 79 | } 80 | 81 | SECTION("change is reflected in binary node") 82 | { 83 | auto val = value { 5 }; 84 | auto node = expression_node { 85 | [](auto a, auto b) { return a + b; }, 86 | expression_node { val }, 87 | expression_node { 7 } 88 | }; 89 | val = 3; 90 | node.eval(); 91 | 92 | REQUIRE(node.get() == 3 + 7); 93 | } 94 | 95 | SECTION("enclose value node can be evaluated") 96 | { 97 | struct enclosed { 98 | value val { 5 }; 99 | } enc; 100 | 101 | auto node = expression_node { enc.val }; 102 | node.eval(); 103 | 104 | REQUIRE(node.get() == 5); 105 | } 106 | } 107 | 108 | TEST_CASE("expression tree/nodes are evaluated only if dirty", "[expression tree]") 109 | { 110 | SECTION("unary node only evaluates operation if dirty") 111 | { 112 | auto call_count = 0; 113 | auto node = expression_node { 114 | [&](auto val) { ++call_count; return val; }, 115 | expression_node { 5 } 116 | }; 117 | 118 | node.eval(); 119 | node.eval(); 120 | 121 | REQUIRE(call_count == 1); 122 | } 123 | 124 | SECTION("binary node only evaluates operation if dirty") 125 | { 126 | auto call_count = 0; 127 | auto node = expression_node { 128 | [&](auto a, auto b) { ++call_count; return a + b; }, 129 | expression_node { 5 }, 130 | expression_node { 7 } 131 | }; 132 | 133 | node.eval(); 134 | node.eval(); 135 | 136 | REQUIRE(call_count == 1); 137 | } 138 | } 139 | 140 | TEST_CASE("expression tree/nodes and values are safe to move", "[expression tree]") 141 | { 142 | SECTION("constant node can be evaluated after move") 143 | { 144 | auto node = expression_node { 5 }; 145 | auto new_node = std::move(node); 146 | new_node.eval(); 147 | 148 | REQUIRE(new_node.get() == 5); 149 | } 150 | 151 | SECTION("value node can be evaluated after node move") 152 | { 153 | auto val = value { 5 }; 154 | auto node = expression_node { val }; 155 | auto new_node = std::move(node); 156 | new_node.eval(); 157 | 158 | REQUIRE(new_node.get() == 5); 159 | } 160 | 161 | SECTION("value node can be evaluated after value move") 162 | { 163 | auto val = value { 5 }; 164 | auto node = expression_node { val }; 165 | 166 | auto moved_val = std::move(val); 167 | 168 | node.eval(); 169 | REQUIRE(node.get() == 5); 170 | 171 | moved_val = 7; 172 | node.eval(); 173 | 174 | REQUIRE(node.get() == 7); 175 | } 176 | 177 | SECTION("value node can be evaluated after both value and node move") 178 | { 179 | auto val = value { 5 }; 180 | auto node = expression_node { val }; 181 | 182 | auto moved_val = std::move(val); 183 | auto moved_node = std::move(node); 184 | 185 | moved_val = 7; 186 | moved_node.eval(); 187 | 188 | REQUIRE(moved_node.get() == 7); 189 | } 190 | 191 | SECTION("change is reflected in value node after move") 192 | { 193 | auto val = value { 5 }; 194 | auto node = expression_node { val }; 195 | auto new_node = std::move(node); 196 | val = 7; 197 | new_node.eval(); 198 | 199 | REQUIRE(new_node.get() == 7); 200 | } 201 | 202 | SECTION("unary node can be evaluated after move") 203 | { 204 | auto node = expression_node { 205 | [](auto v) { return v * 2; }, 206 | expression_node { 5 } 207 | }; 208 | 209 | auto new_node = std::move(node); 210 | new_node.eval(); 211 | 212 | REQUIRE(new_node.get() == 5 * 2); 213 | } 214 | 215 | SECTION("binary node can be evaluated after move") 216 | { 217 | auto node = expression_node { 218 | [](auto a, auto b) { return a + b; }, 219 | expression_node { 5 }, 220 | expression_node { 7 } 221 | }; 222 | 223 | auto new_node = std::move(node); 224 | new_node.eval(); 225 | 226 | REQUIRE(new_node.get() == 5 + 7); 227 | } 228 | 229 | SECTION("change is reflected in binary node after move") 230 | { 231 | auto val = value { 5 }; 232 | auto node = expression_node { 233 | [](auto a, auto b) { return a + b; }, 234 | expression_node { val }, 235 | expression_node { 7 } 236 | }; 237 | 238 | auto new_node = std::move(node); 239 | val = 3; 240 | new_node.eval(); 241 | 242 | REQUIRE(new_node.get() == 3 + 7); 243 | } 244 | 245 | SECTION("change is reflected in unary node after move") 246 | { 247 | auto val = value { 5 }; 248 | auto node = expression_node { 249 | [](auto v) { return v * 2; }, 250 | expression_node { val } 251 | }; 252 | 253 | auto new_node = std::move(node); 254 | val = 7; 255 | new_node.eval(); 256 | 257 | REQUIRE(new_node.get() == 7 * 2); 258 | } 259 | } 260 | 261 | TEST_CASE("expression tree/value node can be evaluated after value is dead", "[expression tree]") 262 | { 263 | auto node = expression_node { }; 264 | 265 | { 266 | auto val = value { 5 }; 267 | node = expression_node { val }; 268 | val = 7; 269 | } 270 | 271 | node.eval(); 272 | 273 | REQUIRE(node.get() == 5); 274 | } 275 | 276 | TEST_CASE("expression tree/notifications", "[expression tree]") 277 | { 278 | SECTION("change triggers value node notification") 279 | { 280 | auto val = value { 5 }; 281 | auto node = expression_node { val }; 282 | 283 | auto called = false; 284 | node.subscribe([&]() { called = true; }).release(); 285 | val = 7; 286 | 287 | REQUIRE(called); 288 | } 289 | 290 | SECTION("change triggers value node notification after move") 291 | { 292 | auto val = value { 5 }; 293 | auto node = expression_node { val }; 294 | 295 | auto called = false; 296 | node.subscribe([&]() { called = true; }).release(); 297 | 298 | auto new_node = std::move(node); 299 | val = 7; 300 | 301 | REQUIRE(called); 302 | } 303 | } 304 | 305 | } } } 306 | -------------------------------------------------------------------------------- /tests/src/detail/collection.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace observable { namespace detail { namespace test { 11 | 12 | TEST_CASE("collection/default constructor", "[collection]") 13 | { 14 | SECTION("collections are default-constructible") 15 | { 16 | REQUIRE(std::is_nothrow_default_constructible>::value); 17 | } 18 | 19 | SECTION("default-constructed collection is empty") 20 | { 21 | REQUIRE(collection { }.empty()); 22 | } 23 | } 24 | 25 | TEST_CASE("collection/copy operations", "[collection]") 26 | { 27 | SECTION("collections are not copy-constructible") 28 | { 29 | REQUIRE_FALSE(std::is_copy_constructible>::value); 30 | } 31 | 32 | SECTION("collections are not copy-assignable") 33 | { 34 | REQUIRE_FALSE(std::is_copy_assignable>::value); 35 | } 36 | } 37 | 38 | TEST_CASE("collection/move operations", "[collection]") 39 | { 40 | SECTION("collections are not move-constructible") 41 | { 42 | REQUIRE_FALSE(std::is_move_constructible>::value); 43 | } 44 | 45 | SECTION("collections are not move-assignable") 46 | { 47 | REQUIRE_FALSE(std::is_move_assignable>::value); 48 | } 49 | } 50 | 51 | TEST_CASE("collection/insert", "[collection]") 52 | { 53 | collection col; 54 | 55 | SECTION("can insert items") 56 | { 57 | col.insert(5); 58 | } 59 | 60 | SECTION("collection is not empty after insert") 61 | { 62 | col.insert(5); 63 | REQUIRE_FALSE(col.empty()); 64 | } 65 | } 66 | 67 | TEST_CASE("collection/apply", "[collection]") 68 | { 69 | collection col; 70 | 71 | SECTION("can apply functor to elements") 72 | { 73 | auto call_count = 0; 74 | 75 | col.insert(5); 76 | col.insert(6); 77 | col.apply([&](auto) { ++call_count; }); 78 | 79 | REQUIRE(call_count == 2); 80 | } 81 | 82 | SECTION("apply does nothing for empty collection") 83 | { 84 | auto call_count = 0; 85 | col.apply([&](auto) { ++call_count; }); 86 | 87 | REQUIRE(call_count == 0); 88 | } 89 | 90 | SECTION("elements are passed to the apply functor") 91 | { 92 | auto result = 0; 93 | 94 | col.insert(11); 95 | col.insert(7); 96 | col.apply([&](auto v) { result += v; }); 97 | 98 | REQUIRE(result == 11 + 7); 99 | } 100 | 101 | SECTION("apply is nothrow for nothrow functor") 102 | { 103 | auto fun = [](auto) noexcept(true) { }; 104 | 105 | REQUIRE(noexcept(col.apply(fun))); 106 | } 107 | 108 | SECTION("apply is not nothrow for throwing functor") 109 | { 110 | auto fun = [](auto) noexcept(false) { }; 111 | 112 | REQUIRE_FALSE(noexcept(col.apply(fun))); 113 | } 114 | } 115 | 116 | TEST_CASE("collection/remove", "[collection]") 117 | { 118 | SECTION("can remove elements") 119 | { 120 | collection col; 121 | auto call_count = 0; 122 | auto id = col.insert(5); 123 | auto success = col.remove(id); 124 | 125 | REQUIRE(success); 126 | REQUIRE(col.empty()); 127 | 128 | col.apply([&](auto) { ++call_count; }); 129 | 130 | REQUIRE(call_count == 0); 131 | } 132 | 133 | SECTION("remove is nothrow") 134 | { 135 | collection col; 136 | REQUIRE(noexcept(col.remove(collection::id { }))); 137 | } 138 | } 139 | 140 | TEST_CASE("collection/mutations during apply", "[collection]") 141 | { 142 | SECTION("will not call apply for a removed element that has not been applied") 143 | { 144 | collection col; 145 | 146 | auto ids = std::array::id, 3> { }; 147 | for(auto i = 0u; i < ids.size(); ++i) 148 | ids[i] = col.insert(i); 149 | 150 | auto call_count = 0; 151 | col.apply([&](auto j) { 152 | for(auto i = 0u; i < ids.size(); ++i) 153 | if(i != j) 154 | col.remove(ids[i]); 155 | ++call_count; 156 | }); 157 | 158 | REQUIRE(call_count == 1); 159 | } 160 | 161 | SECTION("can remove already applied element") 162 | { 163 | collection col; 164 | 165 | auto ids = std::array::id, 3> { }; 166 | for(auto i = 0u; i < ids.size(); ++i) 167 | ids[i] = col.insert(i); 168 | 169 | col.apply([&](auto i) { col.remove(ids[i]); }); 170 | 171 | REQUIRE(col.empty()); 172 | } 173 | 174 | SECTION("can insert element during apply call") 175 | { 176 | collection col; 177 | col.insert(3); 178 | 179 | auto insert_count = 0; 180 | col.apply([&](auto) { 181 | if(insert_count == 0) 182 | col.insert(7); 183 | ++insert_count; 184 | }); 185 | 186 | auto sum = 0; 187 | col.apply([&](auto v) { sum += v; }); 188 | 189 | REQUIRE(sum == 3 + 7); 190 | } 191 | } 192 | 193 | TEST_CASE("collection/concurrent mutations", "[collection]") 194 | { 195 | SECTION("can insert elements in parallel") 196 | { 197 | // This test seems evil, but it fails with a decent probability. 198 | collection col; 199 | 200 | auto ts = std::vector { }; 201 | auto ref_sum = 0; 202 | std::atomic wait { true }; 203 | 204 | for(auto i = 1; i <= 8; ++i) 205 | { 206 | ref_sum += i; 207 | ts.emplace_back([&, i]() { 208 | while(wait) 209 | ; 210 | col.insert(i); 211 | }); 212 | } 213 | 214 | wait = false; 215 | for(auto && t : ts) 216 | t.join(); 217 | 218 | auto ref_els = std::unordered_set { 1, 2, 3, 4, 5, 6, 7, 8 }; 219 | auto els = std::unordered_set { }; 220 | col.apply([&](auto i) { els.insert(i); }); 221 | 222 | REQUIRE(ref_els == els); 223 | } 224 | 225 | SECTION("can remove elements in parallel") 226 | { 227 | // This test seems evil, but it fails with a decent probability. 228 | collection col; 229 | 230 | auto ts = std::vector { }; 231 | std::array::id>, 8> ids; 232 | std::atomic wait { true }; 233 | 234 | for(auto i = 0u; i < ids.size(); ++i) 235 | { 236 | ids[i] = col.insert(i); 237 | ts.emplace_back([&, i]() { 238 | while(wait) 239 | ; 240 | col.remove(ids[i]); 241 | }); 242 | } 243 | 244 | wait = false; 245 | for(auto && t : ts) 246 | t.join(); 247 | 248 | REQUIRE(col.empty()); 249 | } 250 | 251 | SECTION("can insert and remove in parallel") 252 | { 253 | // This test seems evil, but it fails with a decent probability. 254 | collection col; 255 | 256 | auto ts = std::vector { }; 257 | std::array< 258 | std::array::id>, 8>, 259 | 4> ids; 260 | std::atomic wait { true }; 261 | 262 | for(auto j = 0u; j < 4u; ++j) 263 | { 264 | ts.emplace_back([&, j]() { 265 | while(wait) 266 | ; 267 | for(auto i = 0u; i < 8u; ++i) 268 | ids[j][i] = col.insert(i); 269 | 270 | for(auto i = 0u; i < 8u; ++i) 271 | col.remove(ids[j][i]); 272 | }); 273 | } 274 | 275 | wait = false; 276 | for(auto && t : ts) 277 | t.join(); 278 | 279 | REQUIRE(col.empty()); 280 | } 281 | 282 | SECTION("can remove same node in parallel") 283 | { 284 | collection col; 285 | 286 | std::array::id>, 3> ids; 287 | for(auto i = 0u; i < ids.size(); ++i) 288 | ids[i] = col.insert(i); 289 | 290 | std::atomic wait { true }; 291 | auto ts = std::vector { }; 292 | for(auto j = 0; j < 8; ++j) 293 | ts.emplace_back([&]() { 294 | while(wait) 295 | ; 296 | for(auto i = 0u; i < ids.size(); ++i) 297 | col.remove(ids[i]); 298 | }); 299 | 300 | wait = false; 301 | for(auto && t : ts) 302 | t.join(); 303 | 304 | REQUIRE(col.empty()); 305 | } 306 | } 307 | 308 | } } } 309 | -------------------------------------------------------------------------------- /observable/include/observable/subject.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 12 | 13 | namespace observable { 14 | 15 | //! \cond 16 | template 17 | class subject; 18 | //! \endcond 19 | 20 | //! Store observers and provide a way to notify them when events occur. 21 | //! 22 | //! Observers are objects that satisfy the Callable concept and can be stored 23 | //! inside a ``std::function``. 24 | //! 25 | //! Once you call subscribe(), the observer is said to be subscribed to 26 | //! notifications from the subject. 27 | //! 28 | //! Calling notify(), will call all the currently subscribed observers with the 29 | //! arguments provided to notify(). 30 | //! 31 | //! All methods can be safely called in parallel, from multiple threads. 32 | //! 33 | //! \tparam Args Observer arguments. All observer types must be storable 34 | //! inside a ``std::function``. 35 | //! 36 | //! \warning Even though subjects themselves are safe to use in parallel, 37 | //! observers need to handle being called from multiple threads too. 38 | //! 39 | //! \ingroup observable 40 | template 41 | class subject 42 | { 43 | public: 44 | using observer_type = void(Args ...); 45 | 46 | //! Subscribe an observer to notifications. 47 | //! 48 | //! You can safely call this method in parallel, from multiple threads. 49 | //! 50 | //! This method is reentrant, you can add and remove observers from inside 51 | //! other, running, observers. 52 | //! 53 | //! \param[in] observer An observer callable that will be subscribed to 54 | //! notifications from this subject. 55 | //! 56 | //! \tparam Callable Type of the observer object. This type must satisfy the 57 | //! Callable concept and must be storable inside a 58 | //! ``std::function``. 59 | //! 60 | //! \return An unique subscription that can be used to unsubscribe the 61 | //! provided observer from receiving notifications from this subject. 62 | //! 63 | //! \warning Observers must be valid to be called for as long as they are 64 | //! subscribed and there is a possibility to be called. 65 | //! 66 | //! \warning Observers must be safe to be called in parallel, if the notify() 67 | //! method will be called from multiple threads. 68 | template 69 | auto subscribe(Callable && observer) -> infinite_subscription 70 | { 71 | static_assert(detail::is_compatible_with_observer::value, 72 | "The provided observer object is not callable or not compatible" 73 | " with the subject"); 74 | 75 | assert(observers_); 76 | auto const id = observers_->insert(observer); 77 | 78 | return infinite_subscription { 79 | [id, weak_observers = std::weak_ptr { observers_ }]() { 80 | auto const observers = weak_observers.lock(); 81 | if(!observers) 82 | return; 83 | 84 | observers->remove(id); 85 | } 86 | }; 87 | } 88 | 89 | //! Subscribe an observer to notifications and immediately call it with 90 | //! the provided arguments. 91 | //! 92 | //! This method works exactly like the regular subscribe except it also 93 | //! invokes the observer. 94 | //! 95 | //! If the observer call throws an exception during the initial call, it 96 | //! will not be subscribed. 97 | //! 98 | //! \note The observer will not be subscribed during the initial call. 99 | //! 100 | //! \param[in] observer An observer callable that will be subscribed to 101 | //! notifications from this subject and immediately 102 | //! invoked with the provided arguments. 103 | //! \param[in] arguments Arguments to pass to the observer when called. 104 | //! 105 | //! \tparam Callable Type of the observer callable. This type must satisfy 106 | //! the Callable concept and must be storable inside a 107 | //! ``std::function``. 108 | //! 109 | //! \return An unique subscription that can be used to unsubscribe the 110 | //! provided observer from receiving notifications from this 111 | //! subject. 112 | //! 113 | //! \warning Observers must be valid to be called for as long as they are 114 | //! subscribed and there is a possibility to be called. 115 | //! 116 | //! \warning Observers must be safe to be called in parallel, if the notify() 117 | //! method will be called from multiple threads. 118 | //! 119 | //! \see subscribe() 120 | template 121 | auto subscribe_and_call(Callable && observer, ActualArgs ... arguments) 122 | -> infinite_subscription 123 | { 124 | observer(std::forward(arguments)...); 125 | return subscribe(std::forward(observer)); 126 | } 127 | 128 | //! Notify all currently subscribed observers. 129 | //! 130 | //! This method will block until all subscribed observers are called. The 131 | //! method will call observers one-by-one in an unspecified order. 132 | //! 133 | //! You can safely call this method in parallel, from multiple threads. 134 | //! 135 | //! \note Observers subscribed during a notify call, will not be called as 136 | //! part of the notify call during which they were added. 137 | //! 138 | //! \note Observers removed during the notify call, before they themselves 139 | //! have been called, will not be called. 140 | //! 141 | //! The method is reentrant; you can call notify() from inside a running 142 | //! observer. 143 | //! 144 | //! \param[in] arguments Arguments that will be forwarded to the subscribed 145 | //! observers. 146 | //! 147 | //! \warning All observers that will be called by notify() must remain valid 148 | //! to be called for the duration of the notify() call. 149 | //! 150 | //! \warning If notify() is called from multiple threads, all observers must 151 | //! be safe to call from multiple threads. 152 | void notify(Args ... arguments) const 153 | { 154 | assert(observers_); 155 | observers_->apply([&](auto && observer) { observer(arguments ...); }); 156 | } 157 | 158 | //! Return true if there are no subscribers. 159 | auto empty() const noexcept 160 | { 161 | assert(observers_); 162 | return observers_->empty(); 163 | } 164 | 165 | public: 166 | //! Constructor. Will create an empty subject. 167 | subject() =default; 168 | 169 | //! Subjects are **not** copy-constructible. 170 | subject(subject const &) =delete; 171 | 172 | //! Subjects are **not** copy-assignable. 173 | auto operator=(subject const &) -> subject & =delete; 174 | 175 | //! Subjects are move-constructible. 176 | subject(subject &&) noexcept =default; 177 | 178 | //! Subjects are move-assignable. 179 | auto operator=(subject &&) noexcept -> subject & =default; 180 | 181 | private: 182 | using collection = detail::collection>; 183 | 184 | std::shared_ptr observers_ { std::make_shared() }; 185 | }; 186 | 187 | //! Subject specialization that can be used inside a class, as a member, to 188 | //! prevent external code from calling notify(), but still allow anyone to 189 | //! subscribe. 190 | //! 191 | //! \note Except for the notify() method being private, this specialization is 192 | //! exactly the same as a regular subject. 193 | //! 194 | //! \tparam ObserverType The function type of the observers that will subscribe 195 | //! to notifications. 196 | //! 197 | //! \tparam EnclosingType This type will be declared a friend of the subject and 198 | //! will have access to the notify() method. 199 | //! 200 | //! \see subject 201 | //! \ingroup observable 202 | template 203 | class subject : public subject 204 | { 205 | public: 206 | using subject::subject; 207 | 208 | private: 209 | //! \see subject::notify 210 | using subject::notify; 211 | 212 | friend EnclosingType; 213 | }; 214 | 215 | } 216 | 217 | OBSERVABLE_END_CONFIGURE_WARNINGS 218 | -------------------------------------------------------------------------------- /observable/include/observable/expressions/tree.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 14 | 15 | namespace observable { inline namespace expr { 16 | 17 | //! \cond 18 | template 19 | struct is_expression_node; 20 | //! \endcond 21 | 22 | //! Expression nodes can form a tree to evaluate arbitrary expressions. 23 | //! 24 | //! Expressions are formed from n-ary, user-supplied operators and operands. 25 | //! 26 | //! Operands can be constants, observable values or other expression nodes. 27 | //! 28 | //! The tree will propagate change notifications upwards, so any change to any 29 | //! value contained in the tree will be propagated 30 | //! upward, to the root node. 31 | //! 32 | //! When evaluating the root node, only nodes that have been changed will be 33 | //! evaluated. 34 | //! 35 | //! \warning None of the methods in this class can be safely called concurrently. 36 | //! \ingroup observable_detail 37 | template 38 | class expression_node final 39 | { 40 | public: 41 | //! Create a new node from a constant value. 42 | //! 43 | //! Nodes created with this constructor are called constant nodes. These nodes 44 | //! never notify their subscribers of value changes. 45 | //! 46 | //! \param[in] constant A constant value that will become the node's evaluated 47 | //! value. 48 | template ::value && 50 | !is_expression_node::value>> 51 | explicit expression_node(ValueType && constant) 52 | { 53 | static_assert(std::is_convertible::value, 54 | "ValueType must be convertible to ResultType."); 55 | 56 | data_->result = std::forward(constant); 57 | data_->dirty = false; 58 | data_->eval = []() { }; 59 | } 60 | 61 | //! Create a new node from an observable value. 62 | //! 63 | //! Nodes created with this constructor are called value nodes. These nodes 64 | //! notify their subscribers of value changes as long as the value is alive. 65 | //! 66 | //! \param[in] value An observable value who will become the node's evaluated 67 | //! value. This value needs to be kept alive for at least 68 | //! the duration of this constructor call. 69 | //! 70 | //! \note If the value is destroyed, the node will keep returning the last 71 | //! evaluated value indefinitely. 72 | //! \note If the value is moved, the node will use the new, moved-into, value. 73 | template 74 | explicit expression_node(value & value) 75 | { 76 | static_assert(std::is_convertible::value, 77 | "ValueType must be convertible to ResultType."); 78 | 79 | auto mark_dirty = [d = data_.get()]() { 80 | d->dirty = true; 81 | d->notify(); 82 | }; 83 | 84 | auto update_eval = [d = data_.get()](auto & val) { 85 | d->eval = [=, v = &val]() { 86 | if(!d->dirty) 87 | return; 88 | 89 | d->result = v->get(); 90 | d->dirty = false; 91 | }; 92 | }; 93 | 94 | data_->subs.emplace_back(value.subscribe(mark_dirty)); 95 | update_eval(value); 96 | 97 | data_->subs.emplace_back(value.moved.subscribe(update_eval)); 98 | 99 | data_->subs.emplace_back( 100 | value.destroyed.subscribe([d = data_.get()]() { 101 | d->eval = []() { }; 102 | d->subs.clear(); 103 | })); 104 | 105 | data_->eval(); // So we cache the value in case the observable value dies 106 | // before our node's first eval. 107 | } 108 | 109 | //! Create a new node from a number of nodes and an n-ary operation. 110 | //! 111 | //! Nodes created with this constructor are called n-ary nodes. These nodes 112 | //! notify their subscribers of changes to the child nodes provided to this 113 | //! constructor. 114 | //! 115 | //! \param[in] op An n-ary operation with the signature below: 116 | //! 117 | //! ResultType (ValueType const & ...) 118 | //! 119 | //! \param[in] nodes ... Expression nodes who's valuewWW will be the operand to the 120 | //! operation. 121 | template 122 | explicit expression_node(OpType && op, expression_node && ... nodes) 123 | { 124 | static_assert(std::is_convertible::value, 125 | "Operation must return a type that is convertible to ResultType."); 126 | 127 | subscribe_to_nodes(nodes ...); 128 | 129 | data_->eval = [t = std::make_tuple(std::move(nodes) ...), 130 | o = std::forward(op), 131 | d = data_.get()]() mutable { 132 | if(!d->dirty) 133 | return; 134 | 135 | constexpr auto const size = std::tuple_size::value; 136 | constexpr auto const indices = std::make_index_sequence { }; 137 | 138 | d->result = call_with_tuple(o, t, indices); 139 | d->dirty = false; 140 | }; 141 | 142 | data_->eval(); 143 | } 144 | 145 | //! Execute the stored operation and update the node's result value. 146 | void eval() const { data_->eval(); } 147 | 148 | //! Retrieve the expression node's result value. 149 | //! 150 | //! This call will not evaluate the node, so this value might be stale. You 151 | //! can call eval() to make sure that the expression has an updated result 152 | //! value. 153 | auto get() const { return data_->result; } 154 | 155 | //! Subscribe to change notifications from this node. 156 | template 157 | auto subscribe(Observer && callable) { return data_->subscribe(callable); } 158 | 159 | public: 160 | //! Expression nodes are not default-constructible. 161 | expression_node() =default; 162 | 163 | //! Expression nodes are copy-constructible. 164 | //! 165 | //! \warning This copy constructor will produce a shallow copy. 166 | expression_node(expression_node const &) =default; 167 | 168 | //! Expression nodes are copy-assignable. 169 | //! 170 | //! \warning This copy assignment operator will produce a shallow copy. 171 | auto operator=(expression_node const &) -> expression_node & =default; 172 | 173 | //! Expression nodes are move-constructible. 174 | expression_node(expression_node && other) noexcept =default; 175 | 176 | //! Expression nodes are move-assignable. 177 | auto operator=(expression_node && other) noexcept -> expression_node & =default; 178 | 179 | private: 180 | template 181 | void subscribe_to_nodes(Head & head, Nodes & ... nodes) 182 | { 183 | data_->subs.emplace_back( 184 | head.subscribe([d = data_.get()]() { 185 | d->dirty = true; 186 | d->notify(); 187 | })); 188 | 189 | subscribe_to_nodes(nodes ...); 190 | } 191 | 192 | template 193 | void subscribe_to_nodes() 194 | { 195 | // Do nothing. 196 | } 197 | 198 | template 199 | static auto eval_tuple(Tuple & nodes) -> 200 | std::enable_if_t::value> 201 | { 202 | std::get(nodes).eval(); 203 | eval_tuple(nodes); 204 | } 205 | 206 | template 207 | static auto eval_tuple(Tuple &) -> 208 | std::enable_if_t= std::tuple_size::value> 209 | { 210 | // Do nothing. 211 | } 212 | 213 | template 214 | static auto call_with_tuple(Fun & fun, Tuple & nodes, std::index_sequence) 215 | { 216 | eval_tuple(nodes); 217 | return fun(std::get(nodes).get() ...); 218 | } 219 | 220 | private: 221 | struct data : subject 222 | { 223 | ResultType result; 224 | bool dirty = true; 225 | std::function eval; 226 | std::vector subs; 227 | }; 228 | 229 | std::shared_ptr data_ { std::make_shared() }; 230 | }; 231 | 232 | //! \cond 233 | template 234 | struct is_expression_node_ : std::false_type { }; 235 | 236 | template 237 | struct is_expression_node_> : std::true_type { }; 238 | //! \endcond 239 | 240 | //! Check if a type is an expression node. 241 | //! 242 | //! The static member ``value`` will be true if the provided type is an 243 | //! expression_node. 244 | //! 245 | //! \ingroup observable_detail 246 | template 247 | struct is_expression_node : is_expression_node_> { }; 248 | 249 | } } 250 | 251 | OBSERVABLE_END_CONFIGURE_WARNINGS 252 | -------------------------------------------------------------------------------- /observable/include/observable/expressions/filters.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | OBSERVABLE_BEGIN_CONFIGURE_WARNINGS 9 | 10 | //! Create an expression filter from a callable. 11 | //! 12 | //! Expression filters take expression nodes as arguments and return an expression 13 | //! node. 14 | //! 15 | //! This macro wraps a regular callable (i.e. one that does not work with 16 | //! expression nodes) and turns it into an expression filter. 17 | //! 18 | //! \param NAME The name that will be used for the expression filter. This must 19 | //! be a valid function identifier. 20 | //! \param OP Callable instance that will be converted to an expression filter. 21 | //! This instance must be copy-constructible. 22 | //! 23 | //! \ingroup observable_expressions 24 | #define OBSERVABLE_ADAPT_FILTER(NAME, OP) \ 25 | template \ 26 | inline auto NAME(Args && ... args) \ 27 | -> std::enable_if_t< \ 28 | ::observable::expr::expr_detail::are_any_observable::value, \ 29 | ::observable::expr::expr_detail::result_node_t> \ 30 | { \ 31 | return ::observable::expr::expr_detail::make_node(OP, std::forward(args) ...); \ 32 | } 33 | 34 | //! Create an expression filter from a callable. 35 | //! 36 | //! Expression filters take expression nodes as arguments and return an expression 37 | //! node. 38 | //! 39 | //! This macro wraps a regular callable (i.e. one that does not work with 40 | //! expression nodes) and turns it into an expression filter. 41 | //! 42 | //! \param NAME The name that will be used for the expression filter. This must 43 | //! be a valid function identifier. 44 | //! \param OP Callable instance that will be converted to an expression filter. 45 | //! This instance must be copy-constructible. 46 | //! \tparam T The type parameter for the expression filter. 47 | //! 48 | //! \ingroup observable_expressions 49 | #define OBSERVABLE_ADAPT_FILTER_TEMPLATE(NAME, OP) \ 50 | template \ 51 | inline auto NAME(Args && ... args) \ 52 | -> std::enable_if_t< \ 53 | ::observable::expr::expr_detail::are_any_observable::value, \ 54 | ::observable::expr::expr_detail::result_node_t), Args ...>> \ 55 | { \ 56 | return ::observable::expr::expr_detail::make_node(OP, std::forward(args) ...); \ 57 | } 58 | 59 | namespace observable { inline namespace expr { 60 | 61 | //! \cond 62 | namespace filter_detail { 63 | 64 | template 65 | struct construct 66 | { 67 | template 68 | auto operator()(T && ... t) const 69 | { 70 | return ValueType { std::forward(t) ... }; 71 | } 72 | }; 73 | 74 | } 75 | //! \endcond 76 | 77 | //! Construct an object. 78 | //! 79 | //! \param args Arguments to be passed to the object's constructor. You must 80 | //! have at least one observable argument. 81 | //! \return Expression node having the newly constructed object as its result. 82 | //! \tparam ValueType The constructed object's type. 83 | //! \tparam Args Argument pack for the parameters passed to the constructor. 84 | //! \ingroup observable_expressions 85 | template 86 | inline auto construct(Args && ... args) 87 | -> std::enable_if_t::value, 88 | expression_node> 89 | { 90 | return expr_detail::make_node(filter_detail::construct { }, 91 | std::forward(args) ...); 92 | } 93 | 94 | //! Cast an expression node to another type using static_cast. 95 | //! 96 | //! \param from Incoming type. 97 | //! \return Expression node having the To type. 98 | //! 99 | //! \ingroup observable_expressions 100 | template 101 | inline auto static_expr_cast(From && from) 102 | -> std::enable_if_t< 103 | expr_detail::is_observable::value, 104 | expression_node> 105 | { 106 | return expr_detail::make_node([](auto && f) { return static_cast(f); }, 107 | std::forward(from)); 108 | } 109 | 110 | //! Cast an expression node to another type using reinterpret_cast. 111 | //! 112 | //! \param from Incoming type. 113 | //! \return Expression node having the To type. 114 | //! 115 | //! \ingroup observable_expressions 116 | template 117 | inline auto reinterpret_expr_cast(From && from) 118 | -> std::enable_if_t< 119 | expr_detail::is_observable::value, 120 | expression_node> 121 | { 122 | return expr_detail::make_node([](auto && f) { return reinterpret_cast(f); }, 123 | std::forward(from)); 124 | } 125 | 126 | //! \cond 127 | namespace filter_detail { 128 | struct select_ 129 | { 130 | template 133 | auto operator()(CondType && cond, 134 | TrueType && true_val, 135 | FalseType && false_val) const 136 | { 137 | return cond ? true_val : false_val; 138 | } 139 | }; 140 | 141 | struct min_ 142 | { 143 | template 144 | auto operator()(Args && ... args) const 145 | { 146 | using std::min; 147 | return min({ std::forward(args) ... }); 148 | } 149 | }; 150 | 151 | struct max_ 152 | { 153 | template 154 | auto operator()(Args && ... args) const 155 | { 156 | using std::max; 157 | return max({ std::forward(args) ... }); 158 | } 159 | }; 160 | 161 | struct mean_ 162 | { 163 | template 164 | auto operator()(Args && ... args) const 165 | { 166 | return sum(std::forward(args) ...) / static_cast(sizeof...(Args)); 167 | } 168 | 169 | private: 170 | template 171 | auto sum(A && a, Rest && ... rest) const 172 | { 173 | return a + sum(std::forward(rest) ...); 174 | } 175 | 176 | template 177 | auto sum(A && a) const 178 | { 179 | return a; 180 | } 181 | }; 182 | 183 | struct clamp_ 184 | { 185 | template 186 | auto operator()(Val && val, Low && low, High && high) const 187 | { 188 | using std::min; 189 | using std::max; 190 | return min(high, max(low, val)); 191 | } 192 | }; 193 | 194 | struct zip_ 195 | { 196 | template 197 | auto operator()(Args && ... args) const 198 | { 199 | using std::make_tuple; 200 | return make_tuple(std::forward(args) ...); 201 | } 202 | }; 203 | } 204 | //! \endcond 205 | 206 | #if defined(DOXYGEN) 207 | //! Select between two values based on a condition. 208 | //! 209 | //! This is basically the ternary operator for expressions. 210 | //! 211 | //! \param cond A value that evaluates to true or false. This will be used to 212 | //! choose the return value. 213 | //! \param true_val Value that will be returned if ``cond`` evaluates to true. 214 | //! \param false_val Value that will be returned if ``cond`` evaluates to false. 215 | //! \return One of true_val or false_val. 216 | //! 217 | //! \ingroup observable_expressions 218 | template 219 | auto select(Cond && cond, TrueVal && true_val, FalseVal && false_val); 220 | #endif 221 | OBSERVABLE_ADAPT_FILTER(select, filter_detail::select_ { }) 222 | 223 | #if defined(DOXYGEN) 224 | //! Return the argument with the minimum value. 225 | //! 226 | //! Will use the less-than operator to compare values. 227 | //! 228 | //! \param values ... Arguments from which the minimum will be chosen. 229 | //! \return The minimum argument. 230 | //! 231 | //! \ingroup observable_expressions 232 | template 233 | auto min(Values ... values); 234 | #endif 235 | OBSERVABLE_ADAPT_FILTER(min, filter_detail::min_ { }) 236 | 237 | #if defined(DOXYGEN) 238 | //! Return the argument with the maximum value. 239 | //! 240 | //! Will use the less-than operator to compare values. 241 | //! 242 | //! \param values ... Arguments from which the maximum will be chosen. 243 | //! \return The maximum argument. 244 | //! 245 | //! \ingroup observable_expressions 246 | template 247 | auto max(Values ... values); 248 | #endif 249 | OBSERVABLE_ADAPT_FILTER(max, filter_detail::max_ { }) 250 | 251 | #if defined(DOXYGEN) 252 | //! Return the mean of the arguments. 253 | //! 254 | //! \param values ... Arguments for which the mean will be computed. 255 | //! \return The mean value of the arguments. 256 | //! 257 | //! \ingroup observable_expressions 258 | template 259 | auto mean(Values ... values); 260 | #endif 261 | OBSERVABLE_ADAPT_FILTER(mean, filter_detail::mean_ { }) 262 | 263 | #if defined(DOXYGEN) 264 | //! Keep a value between a minimum and maximum. 265 | //! 266 | //! Will use the less-than operator to compare values. 267 | //! 268 | //! \param val Value to clamp. 269 | //! \param low The minimum allowed value of the ``val`` parameter. 270 | //! \param high The maximum allowed value of the ``val`` parameter. 271 | //! \return The clamped value. 272 | //! 273 | //! \ingroup observable_expressions 274 | template 275 | auto clamp(Val && val, Low && low, High && high); 276 | #endif 277 | OBSERVABLE_ADAPT_FILTER(clamp, filter_detail::clamp_ { }) 278 | 279 | #if defined(DOXYGEN) 280 | //! Convert a number of arguments to a tuple containing the arguments. 281 | //! 282 | //! \param args ... Arguments to pack into a tuple. 283 | //! \return Tuple containing the provided arguments. 284 | //! 285 | //! \ingroup observable_expressions 286 | template 287 | auto zip(Args && ... args); 288 | #endif 289 | OBSERVABLE_ADAPT_FILTER(zip, filter_detail::zip_ { }) 290 | 291 | } } 292 | 293 | OBSERVABLE_END_CONFIGURE_WARNINGS 294 | --------------------------------------------------------------------------------