From e63636e3d32c209a0d3de115e61d3a68c2c80710 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Tue, 4 Nov 2025 13:05:01 +0100 Subject: [PATCH 01/16] fix: get project compiled on msvc and clang * silence shadowing because that happens a lot in future.hpp * mark variables as maybe_unused because msvc complains in unused case of constexpr branch * enable __cplusplus makro for msvc because it is used inside the future library but not enabled by default --- cmake/CompilerWarnings.cmake | 2 +- cmake/StandardProjectSettings.cmake | 4 ++++ test/initial_draft.cpp | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index 3786c25..b1f5a5b 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -42,7 +42,7 @@ function( set(CLANG_WARNINGS -Wall -Wextra # reasonable and standard - -Wshadow # warn the user if a variable declaration shadows one from a parent context + #-Wshadow # warn the user if a variable declaration shadows one from a parent context -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps # catch hard to track down memory errors -Wold-style-cast # warn for c-style casts diff --git a/cmake/StandardProjectSettings.cmake b/cmake/StandardProjectSettings.cmake index b9f4123..3f45a08 100644 --- a/cmake/StandardProjectSettings.cmake +++ b/cmake/StandardProjectSettings.cmake @@ -39,6 +39,10 @@ else() message(STATUS "No colored compiler diagnostic set for '${CMAKE_CXX_COMPILER_ID}' compiler.") endif() +# Enable __cplusplus makro on MSVC which is disabled by default +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_compile_options($<$:/Zc:__cplusplus>) +endif() # run vcvarsall when msvc is used include("${CMAKE_CURRENT_LIST_DIR}/VCEnvironment.cmake") diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 339ea8a..d0c21bc 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -123,7 +123,7 @@ class chain { /// for computing the result type of this chain. static consteval auto result_type_helper(Tail&& tail, segment&& head) { return detail::fold_over( - [](auto fold, auto&& first, auto&&... rest) { + []([[maybe_unused]] auto fold, auto&& first, auto&&... rest) { if constexpr (sizeof...(rest) == 0) { return [_segment = STLAB_FWD(first)](auto&&... args) mutable { return std::move(_segment).result_type_helper(STLAB_FWD(args)...); @@ -141,7 +141,7 @@ class chain { template auto expand(const R& receiver) && { return detail::fold_over( - [receiver](auto fold, auto&& first, auto&&... rest) { + [receiver]([[maybe_unused]] auto fold, auto&& first, auto&&... rest) { if constexpr (sizeof...(rest) == 0) { return [receiver, _segment = STLAB_FWD(first).append(receiver)](auto&&... args) mutable { From 58bb11a503ad49eec1f18fd04abe92532badb4f0 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Tue, 4 Nov 2025 14:03:13 +0100 Subject: [PATCH 02/16] Remove cli as dependency because it is not needed and the referenced version fails to compile with CMake 4.x --- Dependencies.cmake | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Dependencies.cmake b/Dependencies.cmake index 2978210..2b4e2b5 100644 --- a/Dependencies.cmake +++ b/Dependencies.cmake @@ -36,10 +36,6 @@ endif() cpmaddpackage("gh:catchorg/Catch2@3.3.2") endif() - if(NOT TARGET CLI11::CLI11) - cpmaddpackage("gh:CLIUtils/CLI11@2.3.2") - endif() - if(NOT TARGET ftxui::screen) cpmaddpackage("gh:ArthurSonzogni/FTXUI#e23dbc7473654024852ede60e2121276c5aab660") endif() From 6e0594ea76953ac28dde0e2291446b613bbfb2f6 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Wed, 5 Nov 2025 09:50:23 +0100 Subject: [PATCH 03/16] Add tuple code --- include/chains/tuple.hpp | 101 +++++++++++++++++++++++++++++++++++---- test/tuple_tests.cpp | 33 +++++++++++++ 2 files changed, 124 insertions(+), 10 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 2f54238..e93cc7f 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -1,7 +1,9 @@ -#include // std::apply, std::forward_as_tuple, std::tuple -#include // std::is_same_v -#include // std::forward, std::move +#include // std::tuple, std::get, std::apply, std::tuple_size_v +#include // std::is_same_v, std::decay_t, std::is_void_v +#include // std::forward, std::move, std::index_sequence, std::make_index_sequence #include // std::monostate +#include // std::invoke +#include // std::size_t #ifndef CHAIN_TUPLE_HPP #define CHAIN_TUPLE_HPP @@ -10,15 +12,12 @@ namespace chains::inline v0 { namespace detail { -/* - Operators for fold expression for sequential execution. Simpler way? -*/ - +/* Map void return to std::monostate */ template inline auto void_to_monostate(F& f) { return [&_f = f](auto&&... args) mutable { - if constexpr (std::is_same_v(args)...)), - void>) { + if constexpr (std::is_same_v< + decltype(std::move(_f)(std::forward(args)...)), void>) { std::move(_f)(std::forward(args)...); return std::monostate{}; } else { @@ -30,7 +29,7 @@ inline auto void_to_monostate(F& f) { template struct tuple_pipeable { T _value; - tuple_pipeable(T&& a) : _value{std::move(a)} {} + explicit tuple_pipeable(T&& a) : _value{std::move(a)} {} }; template @@ -38,6 +37,59 @@ auto operator|(tuple_pipeable&& p, F& f) { return tuple_pipeable{void_to_monostate(f)(std::move(p._value))}; } +/* Check if F is invocable with first K elements of tuple T */ +template +constexpr bool invocable_with_prefix(std::index_sequence) { + return requires(F&& f, T& tup) { std::invoke(f, std::get(tup)...); }; +} + +/* Find largest prefix size (0..N) for which F is invocable */ +template +struct find_max_prefix { + static constexpr std::size_t value = + invocable_with_prefix(std::make_index_sequence{}) + ? N + : find_max_prefix::value; +}; + +template +struct find_max_prefix { + static constexpr std::size_t value = 0; +}; + +/* Invoke F with first K elements of tuple t (K known at compile time) */ +template +auto invoke_prefix(F&& f, Tuple& t) { + if constexpr (K == 0) { + // No arguments: only attempt if callable with () + if constexpr (requires(F&& f2) { std::invoke(f2); }) { + if constexpr (std::is_void_v) { + std::invoke(f); + return std::monostate{}; + } else { + return std::invoke(f); + } + } else { + return std::monostate{}; + } + } else { + return [&](std::index_sequence) { + if constexpr (std::is_void_v(t)...))>) { + std::invoke(f, std::get(t)...); + return std::monostate{}; + } else { + return std::invoke(f, std::get(t)...); + } + }(std::make_index_sequence{}); + } +} + +/* Construct tuple tail starting at Offset (compile time) */ +template +auto tail_from(Tuple& t, std::index_sequence) { + return std::tuple{std::get(t)...}; +} + } // namespace detail //-------------------------------------------------------------------------------------------------- @@ -56,6 +108,35 @@ auto tuple_compose(std::tuple&& sequence) { }; } +//-------------------------------------------------------------------------------------------------- +/* + tuple_consume: + Invokes the given callable with the largest invocable prefix of the stored tuple. + Returns pair. + - If no prefix is invocable, result is std::monostate and remaining_tuple is the original tuple. + - If callable returns void, result is std::monostate. +*/ +template +auto tuple_consume(Tuple&& values) { + return [_values = std::forward(values)](auto&& f) mutable { + using tuple_t = std::decay_t; + constexpr std::size_t N = std::tuple_size_v; + using F_t = decltype(f); + + constexpr std::size_t consumed = detail::find_max_prefix::value; + auto result = detail::invoke_prefix(std::forward(f), _values); + + if constexpr (consumed == 0) { + // Remaining is original tuple (no elements consumed) + return std::pair{std::move(result), _values}; + } else { + auto remaining = + detail::tail_from(_values, std::make_index_sequence{}); + return std::pair{std::move(result), std::move(remaining)}; + } + }; +} + //-------------------------------------------------------------------------------------------------- } // namespace chains::inline v0 diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index db7751c..dd88658 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -13,3 +13,36 @@ TEST_CASE("Test tuple compose", "[tuple_compose]") { [](double x) { return std::to_string(x / 2.0); }}; REQUIRE(chains::tuple_compose(std::move(t))(1) == "2.000000"); } + +struct multi_callable { + int operator()(auto a, auto b) { return a + b; } + int operator()(auto a, auto b, auto c) { return static_cast(a + b + c); } +}; + +struct void_t {}; + +TEST_CASE("Test tuple consume", "[tuple_consume]") { + + GIVEN("A tuple of mixed types") + { + std::tuple t{1, 2.0f, 3, 4.0f}; + + THEN("it returns the correct result for a lambda with two parameters") + { + auto func = [](int a, float b) { return a + static_cast(b); }; + auto result = chains::tuple_consume(t)(func); + REQUIRE((result == std::make_pair(3, std::make_tuple(3, 4.0f)))); + } + THEN("it returns the correct result for a lambda with no parameters") { + auto func = []() { return 42; }; + auto result = chains::tuple_consume(t)(func); + REQUIRE((result == std::make_pair(42, std::make_tuple(1, 2.0f, 3, 4.0f)))); + } + THEN("it returns the correct result for a callable with various call operators") { + auto result = chains::tuple_consume(t)(multi_callable{}); + REQUIRE((result == std::make_pair(6, std::make_tuple(4.0f)))); + } + } + +} + From 8fe86497b6c66abff5a70d217d9eada53296fc47 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Mon, 20 Oct 2025 21:14:46 -0700 Subject: [PATCH 04/16] Cleanup --- .clang-tidy | 42 +-- CMakeLists.txt | 57 ++-- Dependencies.cmake | 42 +-- ProjectOptions.cmake | 16 +- cmake/LibFuzzer.cmake | 17 -- fuzz_test/CMakeLists.txt | 21 -- fuzz_test/fuzz_tester.cpp | 20 -- include/chains/sample_library.hpp | 16 -- src/CMakeLists.txt | 2 - src/ftxui_sample/CMakeLists.txt | 19 -- src/ftxui_sample/main.cpp | 375 -------------------------- src/sample_library/CMakeLists.txt | 27 -- src/sample_library/sample_library.cpp | 12 - test/CMakeLists.txt | 2 +- test/initial_draft.cpp | 3 +- test/tuple_tests.cpp | 5 +- 16 files changed, 82 insertions(+), 594 deletions(-) delete mode 100644 cmake/LibFuzzer.cmake delete mode 100644 fuzz_test/CMakeLists.txt delete mode 100644 fuzz_test/fuzz_tester.cpp delete mode 100644 include/chains/sample_library.hpp delete mode 100644 src/ftxui_sample/CMakeLists.txt delete mode 100644 src/ftxui_sample/main.cpp delete mode 100644 src/sample_library/CMakeLists.txt delete mode 100644 src/sample_library/sample_library.cpp diff --git a/.clang-tidy b/.clang-tidy index cdfaf8b..f002604 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,30 +1,14 @@ ---- -Checks: "*, - -abseil-*, - -altera-*, - -android-*, - -fuchsia-*, - -google-*, - -llvm*, - -modernize-use-trailing-return-type, - -zircon-*, - -readability-else-after-return, - -readability-static-accessed-through-instance, - -readability-avoid-const-params-in-decls, - -cppcoreguidelines-non-private-member-variables-in-classes, - -misc-non-private-member-variables-in-classes, -" -WarningsAsErrors: '' -HeaderFilterRegex: '' -FormatStyle: none - +Checks: > + cert-*, + performance-*, + modernize-*, + misc-include-cleaner, + -readability-identifier-length, + -readability-magic-numbers, + -cppcoreguidelines-avoid-magic-numbers CheckOptions: - - key: readability-identifier-length.IgnoredVariableNames - value: 'x|y|z' - - key: readability-identifier-length.IgnoredParameterNames - value: 'x|y|z' - - - - - + - key: misc-include-cleaner.IgnoreHeaders + value: > + boost/test/.*; + __chrono/.* +HeaderFilterRegex: '^.*/include/chains/.*$' diff --git a/CMakeLists.txt b/CMakeLists.txt index 64a9c00..96eb756 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,15 @@ add_library(chains::chains_warnings ALIAS chains_warnings) #add_library(chains::chains_options INTERFACE IMPORTED) #add_library(chains::chains_warnings INTERFACE IMPORTED) + +add_library(chains INTERFACE) +add_library(chains::chains ALIAS stlab) + +target_include_directories(stlab INTERFACE + $/include + $/include + $) + # configure files based on CMake configuration options add_subdirectory(configured_files) @@ -74,14 +83,14 @@ if(BUILD_TESTING) endif() -if(chains_BUILD_FUZZ_TESTS) - message(AUTHOR_WARNING "Building Fuzz Tests, using fuzzing sanitizer https://www.llvm.org/docs/LibFuzzer.html") - if (NOT chains_ENABLE_ADDRESS_SANITIZER AND NOT chains_ENABLE_THREAD_SANITIZER) - message(WARNING "You need asan or tsan enabled for meaningful fuzz testing") - endif() - add_subdirectory(fuzz_test) +# if(chains_BUILD_FUZZ_TESTS) +# message(AUTHOR_WARNING "Building Fuzz Tests, using fuzzing sanitizer https://www.llvm.org/docs/LibFuzzer.html") +# if (NOT chains_ENABLE_ADDRESS_SANITIZER AND NOT chains_ENABLE_THREAD_SANITIZER) +# message(WARNING "You need asan or tsan enabled for meaningful fuzz testing") +# endif() +# add_subdirectory(fuzz_test) -endif() +# endif() # If MSVC is being used, and ASAN is enabled, we need to set the debugger environment # so that it behaves well with MSVC's debugger, and we can run the target from visual studio @@ -92,30 +101,30 @@ if(MSVC) endif() # set the startup project for the "play" button in MSVC -set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT intro) +# set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT intro) -if(CMAKE_SKIP_INSTALL_RULES) - return() -endif() +# if(CMAKE_SKIP_INSTALL_RULES) +# return() +# endif() -include(cmake/PackageProject.cmake) +# include(cmake/PackageProject.cmake) # Add other targets that you want installed here, by default we just package the one executable # we know we want to ship -chains_package_project( - TARGETS - intro - chains_options - chains_warnings - # FIXME: this does not work! CK - # PRIVATE_DEPENDENCIES_CONFIGURED project_options project_warnings -) +# chains_package_project( +# TARGETS +# intro +# chains_options +# chains_warnings +# # FIXME: this does not work! CK +# # PRIVATE_DEPENDENCIES_CONFIGURED project_options project_warnings +# ) # Experience shows that explicit package naming can help make it easier to sort # out potential ABI related issues before they start, while helping you # track a build to a specific GIT SHA -set(CPACK_PACKAGE_FILE_NAME - "${CMAKE_PROJECT_NAME}-${CMAKE_PROJECT_VERSION}-${GIT_SHORT_SHA}-${CMAKE_SYSTEM_NAME}-${CMAKE_BUILD_TYPE}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}" -) +# set(CPACK_PACKAGE_FILE_NAME +# "${CMAKE_PROJECT_NAME}-${CMAKE_PROJECT_VERSION}-${GIT_SHORT_SHA}-${CMAKE_SYSTEM_NAME}-${CMAKE_BUILD_TYPE}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}" +# ) -include(CPack) +# include(CPack) diff --git a/Dependencies.cmake b/Dependencies.cmake index 2b4e2b5..6d60004 100644 --- a/Dependencies.cmake +++ b/Dependencies.cmake @@ -11,34 +11,38 @@ function(chains_setup_dependencies) if(NOT TARGET stlab::stlab) cpmaddpackage( NAME stlab - VERSION 2.0.0a3 + VERSION 2.0.0a9 GITHUB_REPOSITORY "stlab/libraries" OPTIONS "BUILD_TESTING OFF") endif() - if(NOT TARGET fmtlib::fmtlib) - cpmaddpackage("gh:fmtlib/fmt#9.1.0") - endif() - - if(NOT TARGET spdlog::spdlog) - cpmaddpackage( - NAME - spdlog - VERSION - 1.11.0 - GITHUB_REPOSITORY - "gabime/spdlog" - OPTIONS - "SPDLOG_FMT_EXTERNAL ON") - endif() + # if(NOT TARGET fmtlib::fmtlib) + # cpmaddpackage("gh:fmtlib/fmt#9.1.0") + # endif() + + # if(NOT TARGET spdlog::spdlog) + # cpmaddpackage( + # NAME + # spdlog + # VERSION + # 1.11.0 + # GITHUB_REPOSITORY + # "gabime/spdlog" + # OPTIONS + # "SPDLOG_FMT_EXTERNAL ON") + # endif() if(NOT TARGET Catch2::Catch2WithMain) cpmaddpackage("gh:catchorg/Catch2@3.3.2") endif() - if(NOT TARGET ftxui::screen) - cpmaddpackage("gh:ArthurSonzogni/FTXUI#e23dbc7473654024852ede60e2121276c5aab660") - endif() + # if(NOT TARGET CLI11::CLI11) + # cpmaddpackage("gh:CLIUtils/CLI11@2.3.2") + # endif() + + # if(NOT TARGET ftxui::screen) + # cpmaddpackage("gh:ArthurSonzogni/FTXUI#e23dbc7473654024852ede60e2121276c5aab660") + # endif() if(NOT TARGET tools::tools) cpmaddpackage("gh:lefticus/tools#update_build_system") diff --git a/ProjectOptions.cmake b/ProjectOptions.cmake index 19237f5..409d338 100644 --- a/ProjectOptions.cmake +++ b/ProjectOptions.cmake @@ -1,5 +1,5 @@ include(cmake/SystemLink.cmake) -include(cmake/LibFuzzer.cmake) +# include(cmake/LibFuzzer.cmake) include(CMakeDependentOption) include(CheckCXXCompilerFlag) @@ -78,14 +78,14 @@ macro(chains_setup_options) chains_ENABLE_CACHE) endif() - chains_check_libfuzzer_support(LIBFUZZER_SUPPORTED) - if(LIBFUZZER_SUPPORTED AND (chains_ENABLE_SANITIZER_ADDRESS OR chains_ENABLE_SANITIZER_THREAD OR chains_ENABLE_SANITIZER_UNDEFINED)) - set(DEFAULT_FUZZER ON) - else() - set(DEFAULT_FUZZER OFF) - endif() + # chains_check_libfuzzer_support(LIBFUZZER_SUPPORTED) + # if(LIBFUZZER_SUPPORTED AND (chains_ENABLE_SANITIZER_ADDRESS OR chains_ENABLE_SANITIZER_THREAD OR chains_ENABLE_SANITIZER_UNDEFINED)) + # set(DEFAULT_FUZZER ON) + # else() + # set(DEFAULT_FUZZER OFF) + # endif() - option(chains_BUILD_FUZZ_TESTS "Enable fuzz testing executable" ${DEFAULT_FUZZER}) + # option(chains_BUILD_FUZZ_TESTS "Enable fuzz testing executable" ${DEFAULT_FUZZER}) endmacro() diff --git a/cmake/LibFuzzer.cmake b/cmake/LibFuzzer.cmake deleted file mode 100644 index 60538be..0000000 --- a/cmake/LibFuzzer.cmake +++ /dev/null @@ -1,17 +0,0 @@ -function(chains_check_libfuzzer_support var_name) - set(LibFuzzerTestSource - " -#include - -extern \"C\" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size) { - return 0; -} - ") - - include(CheckCXXSourceCompiles) - - set(CMAKE_REQUIRED_FLAGS "-fsanitize=fuzzer") - set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=fuzzer") - check_cxx_source_compiles("${LibFuzzerTestSource}" ${var_name}) - -endfunction() diff --git a/fuzz_test/CMakeLists.txt b/fuzz_test/CMakeLists.txt deleted file mode 100644 index 56bd81f..0000000 --- a/fuzz_test/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# A fuzz test runs until it finds an error. This particular one is going to rely on libFuzzer. -# - -find_package(fmt) - -add_executable(fuzz_tester fuzz_tester.cpp) -target_link_libraries( - fuzz_tester - PRIVATE chains_options - chains_warnings - fmt::fmt - -coverage - -fsanitize=fuzzer) -target_compile_options(fuzz_tester PRIVATE -fsanitize=fuzzer) - -# Allow short runs during automated testing to see if something new breaks -set(FUZZ_RUNTIME - 10 - CACHE STRING "Number of seconds to run fuzz tests during ctest run") # Default of 10 seconds - -add_test(NAME fuzz_tester_run COMMAND fuzz_tester -max_total_time=${FUZZ_RUNTIME}) diff --git a/fuzz_test/fuzz_tester.cpp b/fuzz_test/fuzz_tester.cpp deleted file mode 100644 index 29d698b..0000000 --- a/fuzz_test/fuzz_tester.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include -#include - -[[nodiscard]] auto sum_values(const uint8_t* Data, size_t Size) { - constexpr auto scale = 1000; - - int value = 0; - for (std::size_t offset = 0; offset < Size; ++offset) { - value += static_cast(*std::next(Data, static_cast(offset))) * scale; - } - return value; -} - -// Fuzzer that attempts to invoke undefined behavior for signed integer overflow -// cppcheck-suppress unusedFunction symbolName=LLVMFuzzerTestOneInput -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* Data, size_t Size) { - fmt::print("Value sum: {}, len{}\n", sum_values(Data, Size), Size); - return 0; -} diff --git a/include/chains/sample_library.hpp b/include/chains/sample_library.hpp deleted file mode 100644 index 22afe40..0000000 --- a/include/chains/sample_library.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef SAMPLE_LIBRARY_HPP -#define SAMPLE_LIBRARY_HPP - -#include - -[[nodiscard]] SAMPLE_LIBRARY_EXPORT int factorial(int) noexcept; - -[[nodiscard]] constexpr int factorial_constexpr(int input) noexcept { - if (input == 0) { - return 1; - } - - return input * factorial_constexpr(input - 1); -} - -#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0f92a9d..e69de29 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,2 +0,0 @@ -add_subdirectory(sample_library) -add_subdirectory(ftxui_sample) diff --git a/src/ftxui_sample/CMakeLists.txt b/src/ftxui_sample/CMakeLists.txt deleted file mode 100644 index 264d3d3..0000000 --- a/src/ftxui_sample/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -add_executable(intro main.cpp) - -target_link_libraries( - intro - PRIVATE chains::chains_options - chains::chains_warnings) - -target_link_system_libraries( - intro - PRIVATE - CLI11::CLI11 - fmt::fmt - spdlog::spdlog - lefticus::tools - ftxui::screen - ftxui::dom - ftxui::component) - -target_include_directories(intro PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") diff --git a/src/ftxui_sample/main.cpp b/src/ftxui_sample/main.cpp deleted file mode 100644 index dc6d786..0000000 --- a/src/ftxui_sample/main.cpp +++ /dev/null @@ -1,375 +0,0 @@ -#include -#include -#include -#include - -#include - -#include -#include // for ftxui -#include // for Slider -#include // for ScreenInteractive -#include - -#include - -// This file will be generated automatically when cur_you run the CMake -// configuration step. It creates a namespace called `chains`. You can modify -// the source template at `configured_files/config.hpp.in`. -#include - -template -struct GameBoard { - static constexpr std::size_t width = Width; - static constexpr std::size_t height = Height; - - std::array, width> strings; - std::array, width> values{}; - - std::size_t move_count{0}; - - std::string& get_string(std::size_t cur_x, std::size_t cur_y) { - return strings.at(cur_x).at(cur_y); - } - - void set(std::size_t cur_x, std::size_t cur_y, bool new_value) { - get(cur_x, cur_y) = new_value; - - if (new_value) { - get_string(cur_x, cur_y) = " ON"; - } else { - get_string(cur_x, cur_y) = "OFF"; - } - } - - void visit(auto visitor) { - for (std::size_t cur_x = 0; cur_x < width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height; ++cur_y) { - visitor(cur_x, cur_y, *this); - } - } - } - - [[nodiscard]] bool get(std::size_t cur_x, std::size_t cur_y) const { - return values.at(cur_x).at(cur_y); - } - - [[nodiscard]] bool& get(std::size_t cur_x, std::size_t cur_y) { - return values.at(cur_x).at(cur_y); - } - - GameBoard() { - visit([](const auto cur_x, const auto cur_y, auto& gameboard) { - gameboard.set(cur_x, cur_y, true); - }); - } - - void update_strings() { - for (std::size_t cur_x = 0; cur_x < width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height; ++cur_y) { - set(cur_x, cur_y, get(cur_x, cur_y)); - } - } - } - - void toggle(std::size_t cur_x, std::size_t cur_y) { set(cur_x, cur_y, !get(cur_x, cur_y)); } - - void press(std::size_t cur_x, std::size_t cur_y) { - ++move_count; - toggle(cur_x, cur_y); - if (cur_x > 0) { - toggle(cur_x - 1, cur_y); - } - if (cur_y > 0) { - toggle(cur_x, cur_y - 1); - } - if (cur_x < width - 1) { - toggle(cur_x + 1, cur_y); - } - if (cur_y < height - 1) { - toggle(cur_x, cur_y + 1); - } - } - - [[nodiscard]] bool solved() const { - for (std::size_t cur_x = 0; cur_x < width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height; ++cur_y) { - if (!get(cur_x, cur_y)) { - return false; - } - } - } - - return true; - } -}; - -void consequence_game() { - auto screen = ftxui::ScreenInteractive::TerminalOutput(); - - GameBoard<3, 3> game_board; - - std::string quit_text; - - const auto update_quit_text = [&quit_text](const auto& game_board_param) { - quit_text = fmt::format("Quit ({} moves)", game_board_param.move_count); - if (game_board_param.solved()) { - quit_text += " Solved!"; - } - }; - - const auto make_buttons = [&] { - std::vector buttons; - for (std::size_t cur_x = 0; cur_x < game_board.width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < game_board.height; ++cur_y) { - buttons.push_back( - ftxui::Button(&game_board.get_string(cur_x, cur_y), [=, &game_board] { - if (!game_board.solved()) { - game_board.press(cur_x, cur_y); - } - update_quit_text(game_board); - })); - } - } - return buttons; - }; - - auto buttons = make_buttons(); - - auto quit_button = ftxui::Button(&quit_text, screen.ExitLoopClosure()); - - auto make_layout = [&] { - std::vector rows; - - std::size_t idx = 0; - - for (std::size_t cur_x = 0; cur_x < game_board.width; ++cur_x) { - std::vector row; - for (std::size_t cur_y = 0; cur_y < game_board.height; ++cur_y) { - row.push_back(buttons[idx]->Render()); - ++idx; - } - rows.push_back(ftxui::hbox(std::move(row))); - } - - rows.push_back(ftxui::hbox({quit_button->Render()})); - - return ftxui::vbox(std::move(rows)); - }; - - static constexpr int randomization_iterations = 100; - static constexpr int random_seed = 42; - - std::mt19937 gen32{random_seed}; // NOLINT fixed seed - - // NOLINTNEXTLINE This cannot be const - std::uniform_int_distribution cur_x(static_cast(0), - game_board.width - 1); - // NOLINTNEXTLINE This cannot be const - std::uniform_int_distribution cur_y(static_cast(0), - game_board.height - 1); - - for (int i = 0; i < randomization_iterations; ++i) { - game_board.press(cur_x(gen32), cur_y(gen32)); - } - game_board.move_count = 0; - update_quit_text(game_board); - - auto all_buttons = buttons; - all_buttons.push_back(quit_button); - auto container = ftxui::Container::Horizontal(all_buttons); - - auto renderer = ftxui::Renderer(container, make_layout); - - screen.Loop(renderer); -} - -struct Color { - lefticus::tools::uint_np8_t R{static_cast(0)}; - lefticus::tools::uint_np8_t G{static_cast(0)}; - lefticus::tools::uint_np8_t B{static_cast(0)}; -}; - -// A simple way of representing a bitmap on screen using only characters -struct Bitmap : ftxui::Node { - Bitmap(std::size_t width, - std::size_t height) // NOLINT same typed parameters adjacent to each other - : width_(width), height_(height) {} - - Color& at(std::size_t cur_x, std::size_t cur_y) { return pixels.at(width_ * cur_y + cur_x); } - - void ComputeRequirement() override { - requirement_ = ftxui::Requirement{.min_x = static_cast(width_), - .min_y = static_cast(height_ / 2), - .selected_box{0, 0, 0, 0}}; - } - - void Render(ftxui::Screen& screen) override { - for (std::size_t cur_x = 0; cur_x < width_; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height_ / 2; ++cur_y) { - auto& pixel = screen.PixelAt(box_.x_min + static_cast(cur_x), - box_.y_min + static_cast(cur_y)); - pixel.character = "▄"; - const auto& top_color = at(cur_x, cur_y * 2); - const auto& bottom_color = at(cur_x, cur_y * 2 + 1); - pixel.background_color = - ftxui::Color{top_color.R.get(), top_color.G.get(), top_color.B.get()}; - pixel.foreground_color = - ftxui::Color{bottom_color.R.get(), bottom_color.G.get(), bottom_color.B.get()}; - } - } - } - - [[nodiscard]] auto width() const noexcept { return width_; } - - [[nodiscard]] auto height() const noexcept { return height_; } - - [[nodiscard]] auto& data() noexcept { return pixels; } - -private: - std::size_t width_; - std::size_t height_; - - std::vector pixels = std::vector(width_ * height_, Color{}); -}; - -void game_iteration_canvas() { - // this should probably have a `bitmap` helper function that does what cur_you expect - // similar to the other parts of FTXUI - auto bm = std::make_shared(50, 50); // NOLINT magic numbers - auto small_bm = std::make_shared(6, 6); // NOLINT magic numbers - - double fps = 0; - - std::size_t max_row = 0; - std::size_t max_col = 0; - - // to do, add total game time clock also, not just current elapsed time - auto game_iteration = [&](const std::chrono::steady_clock::duration elapsed_time) { - // in here we simulate however much game time has elapsed. Update animations, - // run character AI, whatever, update stats, etc - - // this isn't actually timing based for now, it's just updating the display however fast it - // can - fps = 1.0 / - (static_cast( - std::chrono::duration_cast(elapsed_time).count()) / - 1'000'000.0); // NOLINT magic numbers - - for (std::size_t row = 0; row < max_row; ++row) { - for (std::size_t col = 0; col < bm->width(); ++col) { - ++(bm->at(col, row).R); - } - } - - for (std::size_t row = 0; row < bm->height(); ++row) { - for (std::size_t col = 0; col < max_col; ++col) { - ++(bm->at(col, row).G); - } - } - - // for the fun of it, let's have a second window doing interesting things - auto& small_bm_pixel = small_bm->data().at(static_cast(elapsed_time.count()) % - small_bm->data().size()); - - switch (elapsed_time.count() % 3) { - case 0: - small_bm_pixel.R += 11; // NOLINT Magic Number - break; - case 1: - small_bm_pixel.G += 11; // NOLINT Magic Number - break; - case 2: - small_bm_pixel.B += 11; // NOLINT Magic Number - break; - } - - ++max_row; - if (max_row >= bm->height()) { - max_row = 0; - } - ++max_col; - if (max_col >= bm->width()) { - max_col = 0; - } - }; - - auto screen = ftxui::ScreenInteractive::TerminalOutput(); - - int counter = 0; - - auto last_time = std::chrono::steady_clock::now(); - - auto make_layout = [&] { - // This code actually processes the draw event - const auto new_time = std::chrono::steady_clock::now(); - - ++counter; - // we will dispatch to the game_iteration function, where the work happens - game_iteration(new_time - last_time); - last_time = new_time; - - // now actually draw the game elements - return ftxui::hbox( - {bm | ftxui::border, - ftxui::vbox({ftxui::text("Frame: " + std::to_string(counter)), - ftxui::text("FPS: " + std::to_string(fps)), small_bm | ftxui::border})}); - }; - - auto renderer = ftxui::Renderer(make_layout); - - std::atomic refresh_ui_continue = true; - - // This thread exists to make sure that the event queue has an event to - // process at approximately a rate of 30 FPS - std::thread refresh_ui([&] { - while (refresh_ui_continue) { - using namespace std::chrono_literals; - std::this_thread::sleep_for(1.0s / 30.0); // NOLINT magic numbers - screen.PostEvent(ftxui::Event::Custom); - } - }); - - screen.Loop(renderer); - - refresh_ui_continue = false; - refresh_ui.join(); -} - -// NOLINTNEXTLINE(bugprone-exception-escape) -int main(int argc, const char** argv) { - try { - CLI::App app{fmt::format("{} version {}", chains::cmake::project_name, - chains::cmake::project_version)}; - - std::optional message; - app.add_option("-m,--message", message, "A message to print back out"); - bool show_version = false; - app.add_flag("--version", show_version, "Show version information"); - - bool is_turn_based = false; - auto* turn_based = app.add_flag("--turn_based", is_turn_based); - - bool is_loop_based = false; - auto* loop_based = app.add_flag("--loop_based", is_loop_based); - - turn_based->excludes(loop_based); - loop_based->excludes(turn_based); - - CLI11_PARSE(app, argc, argv); - - if (show_version) { - fmt::print("{}\n", chains::cmake::project_version); - return EXIT_SUCCESS; - } - - if (is_turn_based) { - consequence_game(); - } else { - game_iteration_canvas(); - } - - } catch (const std::exception& e) { - spdlog::error("Unhandled exception in main: {}", e.what()); - } -} diff --git a/src/sample_library/CMakeLists.txt b/src/sample_library/CMakeLists.txt deleted file mode 100644 index c8fafce..0000000 --- a/src/sample_library/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -include(GenerateExportHeader) - - -add_library(sample_library sample_library.cpp) - - - -add_library(chains::sample_library ALIAS sample_library) - -target_link_libraries(sample_library PRIVATE chains_options chains_warnings) - -target_include_directories(sample_library ${WARNING_GUARD} PUBLIC $ - $) - -target_compile_features(sample_library PUBLIC cxx_std_20) - -set_target_properties( - sample_library - PROPERTIES VERSION ${PROJECT_VERSION} - CXX_VISIBILITY_PRESET hidden - VISIBILITY_INLINES_HIDDEN YES) - -generate_export_header(sample_library EXPORT_FILE_NAME ${PROJECT_BINARY_DIR}/include/chains/sample_library_export.hpp) - -if(NOT BUILD_SHARED_LIBS) - target_compile_definitions(sample_library PUBLIC SAMPLE_LIBRARY_STATIC_DEFINE) -endif() diff --git a/src/sample_library/sample_library.cpp b/src/sample_library/sample_library.cpp deleted file mode 100644 index aaeb999..0000000 --- a/src/sample_library/sample_library.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include - -int factorial(int input) noexcept { - int result = 1; - - while (input > 0) { - result *= input; - --input; - } - - return result; -} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9394e30..281eb03 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,7 +34,7 @@ target_link_libraries( tests PRIVATE chains::chains_warnings chains::chains_options - chains::sample_library + chains::chains stlab::stlab Catch2::Catch2WithMain) diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index d0c21bc..8c7259c 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -5,13 +5,14 @@ #include #include - +#include #include // temporary #include #include #include +#include #define STLAB_FWD(x) std::forward(x) diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index dd88658..3e3ca38 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -6,12 +6,11 @@ #include #include -#include - TEST_CASE("Test tuple compose", "[tuple_compose]") { std::tuple t{[](int x) { return x + 1.0; }, [](double x) { return x * 2.0; }, [](double x) { return std::to_string(x / 2.0); }}; - REQUIRE(chains::tuple_compose(std::move(t))(1) == "2.000000"); + auto f = chains::tuple_compose(std::move(t)); + REQUIRE(f(1) == "2.000000"); } struct multi_callable { From a51e3f82c35d1a8af48d69a559110bc3c8b141f0 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Wed, 5 Nov 2025 18:22:30 +0100 Subject: [PATCH 05/16] Small cleanup for clang++ --- include/chains/tuple.hpp | 17 ++++++++++------- test/initial_draft.cpp | 3 ++- test/tuple_tests.cpp | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index e93cc7f..0b5ce0e 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -39,7 +39,7 @@ auto operator|(tuple_pipeable&& p, F& f) { /* Check if F is invocable with first K elements of tuple T */ template -constexpr bool invocable_with_prefix(std::index_sequence) { +constexpr auto invocable_with_prefix(std::index_sequence) { return requires(F&& f, T& tup) { std::invoke(f, std::get(tup)...); }; } @@ -84,11 +84,6 @@ auto invoke_prefix(F&& f, Tuple& t) { } } -/* Construct tuple tail starting at Offset (compile time) */ -template -auto tail_from(Tuple& t, std::index_sequence) { - return std::tuple{std::get(t)...}; -} } // namespace detail @@ -108,6 +103,14 @@ auto tuple_compose(std::tuple&& sequence) { }; } +//-------------------------------------------------------------------------------------------------- + +/* Construct tuple tail starting at Offset (compile time) */ +template +auto tuple_tail_at(Tuple& t, std::index_sequence) { + return std::tuple{std::move(std::get(t))...}; +} + //-------------------------------------------------------------------------------------------------- /* tuple_consume: @@ -131,7 +134,7 @@ auto tuple_consume(Tuple&& values) { return std::pair{std::move(result), _values}; } else { auto remaining = - detail::tail_from(_values, std::make_index_sequence{}); + tuple_tail_at(_values, std::make_index_sequence{}); return std::pair{std::move(result), std::move(remaining)}; } }; diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 8c7259c..e11c196 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -309,6 +309,7 @@ using namespace chains; using namespace stlab; TEST_CASE("Initial draft", "[initial_draft]") { +#if 0 auto a0 = on(default_executor) | [] { cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; return 42; @@ -350,6 +351,6 @@ TEST_CASE("Initial draft", "[initial_draft]") { // std::this_thread::sleep_for(3s); std::cout << await(std::move(a1)()) << "\n"; - +#endif pre_exit(); } diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index 3e3ca38..0828005 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -14,8 +14,8 @@ TEST_CASE("Test tuple compose", "[tuple_compose]") { } struct multi_callable { - int operator()(auto a, auto b) { return a + b; } - int operator()(auto a, auto b, auto c) { return static_cast(a + b + c); } + int operator()(int a, float b) const { return a + static_cast(b); } + int operator()(int a, float b, int c) const { return a + static_cast(b) + c; } }; struct void_t {}; From 00edd2394f13d5c9ca3eb6c989e1c3fbd964d386 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Wed, 5 Nov 2025 21:56:04 +0100 Subject: [PATCH 06/16] Change the result type that it attaches the new value to the tuple --- include/chains/tuple.hpp | 4 ++-- test/tuple_tests.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 0b5ce0e..4532f6e 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -131,11 +131,11 @@ auto tuple_consume(Tuple&& values) { if constexpr (consumed == 0) { // Remaining is original tuple (no elements consumed) - return std::pair{std::move(result), _values}; + return std::tuple_cat(std::make_tuple(std::move(result)), _values); } else { auto remaining = tuple_tail_at(_values, std::make_index_sequence{}); - return std::pair{std::move(result), std::move(remaining)}; + return std::tuple_cat(std::make_tuple(std::move(result)), std::move(remaining)); } }; } diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index 0828005..bf4853f 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -30,16 +30,16 @@ TEST_CASE("Test tuple consume", "[tuple_consume]") { { auto func = [](int a, float b) { return a + static_cast(b); }; auto result = chains::tuple_consume(t)(func); - REQUIRE((result == std::make_pair(3, std::make_tuple(3, 4.0f)))); + REQUIRE((result == std::make_tuple(3, 3, 4.0f))); } THEN("it returns the correct result for a lambda with no parameters") { auto func = []() { return 42; }; auto result = chains::tuple_consume(t)(func); - REQUIRE((result == std::make_pair(42, std::make_tuple(1, 2.0f, 3, 4.0f)))); + REQUIRE((result == std::make_tuple(42, 1, 2.0f, 3, 4.0f))); } THEN("it returns the correct result for a callable with various call operators") { auto result = chains::tuple_consume(t)(multi_callable{}); - REQUIRE((result == std::make_pair(6, std::make_tuple(4.0f)))); + REQUIRE((result == std::make_tuple(6, 4.0f))); } } From 6e2df59d3ec3b3b5640be7bcdd17f801e079a943 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 7 Nov 2025 13:14:19 +0100 Subject: [PATCH 07/16] Add tech proof that a chain of function calls with different number of arguments can be used and it returns the correct result. Save interim state Next interim --- include/chains/tuple.hpp | 31 +++- test/initial_draft.cpp | 334 +++++++++++++++++++++++++++------------ test/tuple_tests.cpp | 18 ++- 3 files changed, 282 insertions(+), 101 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 4532f6e..42d8eeb 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -59,7 +59,7 @@ struct find_max_prefix { /* Invoke F with first K elements of tuple t (K known at compile time) */ template -auto invoke_prefix(F&& f, Tuple& t) { +constexpr auto invoke_prefix(F&& f, Tuple& t) { if constexpr (K == 0) { // No arguments: only attempt if callable with () if constexpr (requires(F&& f2) { std::invoke(f2); }) { @@ -107,7 +107,7 @@ auto tuple_compose(std::tuple&& sequence) { /* Construct tuple tail starting at Offset (compile time) */ template -auto tuple_tail_at(Tuple& t, std::index_sequence) { +constexpr auto tuple_tail_at(Tuple& t, std::index_sequence) { return std::tuple{std::move(std::get(t))...}; } @@ -120,7 +120,7 @@ auto tuple_tail_at(Tuple& t, std::index_sequence) { - If callable returns void, result is std::monostate. */ template -auto tuple_consume(Tuple&& values) { +constexpr auto tuple_consume(Tuple&& values) { return [_values = std::forward(values)](auto&& f) mutable { using tuple_t = std::decay_t; constexpr std::size_t N = std::tuple_size_v; @@ -140,6 +140,31 @@ auto tuple_consume(Tuple&& values) { }; } +template +struct calculator { + template + static constexpr auto apply(F& f, T t) { + if constexpr (I == std::tuple_size_v) { + return std::get<0>(t); + } else { + return calculator::apply(f, chains::tuple_consume(std::move(t))(std::get(f))); + } + } +}; + +template +constexpr auto calc(F f, Args&&... args) { + auto arguments = std::make_tuple(std::forward(args)...); + return calculator<0>::apply(f, arguments); +} + +template +constexpr auto tuple_compose_greedy(std::tuple&& sequence) { + return [_sequence = std::move(sequence)](auto&&... args) mutable { + return calc(std::move(_sequence), std::forward(args)...); + }; +} + //-------------------------------------------------------------------------------------------------- } // namespace chains::inline v0 diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index e11c196..fc8b18a 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -5,14 +5,13 @@ #include #include -#include + #include // temporary #include #include #include -#include #define STLAB_FWD(x) std::forward(x) @@ -32,20 +31,53 @@ segment is invoked with a receiver - */ -template + +template +struct receiver_ref { + Receiver* _receiver; + void operator()(auto&&... args) { + _receiver->operator()(std::forward(args)...); + } + void set_exception(std::exception_ptr p) { _receiver->set_exception(p); } + bool canceled() const { return _receiver->canceled(); } +}; + +template +struct type {}; + +template +class segment; + +#if 0 +template +inline auto make_segment(Applicator&& apply, Fs&&... fs) { + return segment, std::decay_t...>{ + std::forward(apply), std::forward(fs)...}; +} +#endif + +template class segment { std::tuple _functions; Applicator _apply; public: + /* + An apply operation may inject additional arguments into the segment. The plan is that the + receiver will get sent to apply and this is how cancellation tokens can be injected into an + operation. Something like `with_cancellation`. + + This feature is also used for the `then` operation where the resolve future is injected into + the segment. + */ template auto result_type_helper(Args&&... args) && { - return tuple_compose(std::move(_functions))(std::forward(args)...); + return tuple_compose_greedy(std::move(_functions))(std::forward(args)...); } - explicit segment(Applicator&& apply, std::tuple&& functions) + explicit segment(type, Applicator&& apply, std::tuple&& functions) : _functions{std::move(functions)}, _apply{std::move(apply)} {} - explicit segment(Applicator&& apply, Fs&&... functions) + explicit segment(type, Applicator&& apply, Fs&&... functions) : _functions{std::move(functions)...}, _apply{std::move(apply)} {} /* @@ -59,8 +91,9 @@ class segment { template auto append(F&& f) && { - return chains::segment{std::move(_apply), std::tuple_cat(std::move(_functions), - std::tuple{std::forward(f)})}; + return chains::segment{ + type{}, std::move(_apply), + std::tuple_cat(std::move(_functions), std::make_tuple(std::forward(f)))}; } #if 0 @@ -78,17 +111,17 @@ class segment { */ template - void invoke(const R& receiver, Args&&... args) && { - if (receiver.canceled()) return; - - std::move(_apply)( - [_f = tuple_compose(std::move(_functions)), - _receiver = receiver](auto&&... args) mutable noexcept { - if (_receiver.canceled()) return; + auto invoke(R&& receiver, Args&&... args) && { + // TODO: must handle this cancel prior to invoking the segment. + // if (receiver.canceled()) return; + return std::move(_apply)( + [_f = tuple_compose_greedy(std::move(_functions)), + _receiver = std::forward(receiver)](auto&&... args) mutable noexcept { + if (_receiver->canceled()) return; try { std::move(_f)(std::forward(args)...); } catch (...) { - _receiver.set_exception(std::current_exception()); + _receiver->set_exception(std::current_exception()); } }, std::forward(args)...); @@ -100,31 +133,28 @@ namespace detail { /// Apply a recursive lambda to each element in the tuple-like Segments. template constexpr auto fold_over(Fold fold, Segments&& segments) { - return std::apply([fold](auto&&... links) { return fold(fold, STLAB_FWD(links)...); }, + return std::apply([fold](auto&&... links) mutable { return fold(fold, STLAB_FWD(links)...); }, STLAB_FWD(segments)); } } // namespace detail -template -using segment_result_type = - decltype(std::declval().result_type_helper(std::declval()...)); - /* simplify this code by handing the multi-argument case earlier (somehow). */ -template +template class chain { Tail _tail; - segment _head; + segment _head; /// Return a lambda with the signature of /// head( tail( tail<1>( tail<0>( auto&& args... ) ) ) ) /// for computing the result type of this chain. - static consteval auto result_type_helper(Tail&& tail, segment&& head) { + static consteval auto result_type_helper(Tail&& tail, + segment&& head) { return detail::fold_over( - []([[maybe_unused]] auto fold, auto&& first, auto&&... rest) { + []([[maybe_unused]]auto fold, auto&& first, auto&&... rest) { if constexpr (sizeof...(rest) == 0) { return [_segment = STLAB_FWD(first)](auto&&... args) mutable { return std::move(_segment).result_type_helper(STLAB_FWD(args)...); @@ -136,34 +166,54 @@ class chain { }; } }, - std::tuple_cat(std::move(tail), std::tuple{std::move(head)})); + std::tuple_cat(STLAB_FWD(tail), std::tuple{STLAB_FWD(head)})); } template - auto expand(const R& receiver) && { + auto expand(R&& receiver) && { + using receiver_t = std::decay_t; + auto shared_receiver = std::make_shared(std::forward(receiver)); + return detail::fold_over( - [receiver]([[maybe_unused]] auto fold, auto&& first, auto&&... rest) { + [shared_receiver]([[maybe_unused]] auto& fold, auto&& first, + auto&&... rest) mutable { if constexpr (sizeof...(rest) == 0) { - return [receiver, - _segment = STLAB_FWD(first).append(receiver)](auto&&... args) mutable { - return std::move(_segment).invoke(receiver, STLAB_FWD(args)...); + return [shared_receiver, _segment = STLAB_FWD(first).append(shared_receiver)]( + auto&&... args) mutable { + return std::move(_segment).invoke(shared_receiver, STLAB_FWD(args)...); }; } else { - return [receiver, _segment = STLAB_FWD(first).append( + return [shared_receiver, + _segment = STLAB_FWD(first).append( fold(fold, STLAB_FWD(rest)...))](auto&&... args) mutable { - return std::move(_segment).invoke(receiver, STLAB_FWD(args)...); + return std::move(_segment).invoke(shared_receiver, STLAB_FWD(args)...); }; } }, std::tuple_cat(std::move(_tail), std::tuple{std::move(_head)})); } + template + struct result_type_void_injects { + using type = decltype(result_type_helper( + std::declval(), + std::declval>())(std::declval()...)); + }; + + template + struct result_type_injects { + using type = decltype(result_type_helper( + std::declval(), std::declval>())( + std::declval(), std::declval()...)); + }; + public: template - using result_type = decltype(result_type_helper( - std::declval(), std::declval>())(std::declval()...)); + using result_type = std::conditional_t, + result_type_void_injects, + result_type_injects>::type; - explicit chain(Tail&& tail, segment&& head) + explicit chain(Tail&& tail, segment&& head) : _tail{std::move(tail)}, _head{std::move(head)} {} /* @@ -182,37 +232,46 @@ class chain { return chains::chain{std::move(_tail), std::move(_head).append(std::forward(f))}; } - template - auto append(segment&& head) && { + template + auto append(segment&& head) && { return chains::chain{std::tuple_cat(std::move(_tail), std::make_tuple(std::move(_head))), std::move(head)}; } + template + auto invoke(Receiver&& receiver, Args&&... args) && { + return std::move(*this).expand(std::forward(receiver))( + std::forward(args)...); + } + +#if 0 template - auto operator()(Args&&... args) && { + [[deprecated]] auto operator()(Args&&... args) && { using result_t = result_type; auto [receiver, future] = stlab::package(stlab::immediate_executor, std::identity{}); - (void)std::move(*this).expand(receiver)(std::forward(args)...); + invoke(std::move(receiver), std::forward(args)...); return std::move(future); } +#endif template friend auto operator|(chain&& c, F&& f) { return std::move(c).append(std::forward(f)); } - template - friend auto operator|(chain&& c, segment&& head) { + template + friend auto operator|(chain&& c, segment&& head) { return std::move(c).append(std::move(head)); } }; -template -chain(Tail&& tail, segment&& head) -> chain; +template +chain(Tail&& tail, segment&& head) + -> chain; -template -inline auto operator|(segment&& head, F&& f) { +template +inline auto operator|(segment&& head, F&& f) { return chain{std::tuple<>{}, std::move(head).append(std::forward(f))}; } @@ -225,16 +284,6 @@ inline auto operator|(segment&& head, F&& f) { namespace chains::inline v1 { -#if 0 -template -inline auto on(E&& executor) { - return segment{[_executor = std::forward(executor)](auto&& f, auto&&... args) mutable { - return stlab::async(std::move(_executor), std::forward(f), - std::forward(args)...); - }}; -} -#endif - /* Each segment invokes the next segment with result and returns void. Promise is bound to the @@ -243,62 +292,132 @@ last item in the chain as a segment. */ template inline auto on(E&& executor) { - return segment{[_executor = std::forward(executor)](auto&& f, auto&&... args) mutable { - std::move(_executor)( - [_f = std::forward(f), - _args = std::tuple{std::forward(args)...}]() mutable noexcept { - std::apply(std::move(_f), std::move(_args)); - }); - return std::monostate{}; - }}; + return segment{ + type{}, [_executor = std::forward(executor)](auto&& f, auto&&... args) mutable { + std::move(_executor)( + [_f = std::forward(f), + _args = std::tuple{std::forward(args)...}]() mutable noexcept { + std::apply(std::move(_f), std::move(_args)); + }); + // return std::monostate{}; + }}; } -#if 0 +/* + The `then` algorithm takes a future and returns a segment (chain) that will schedule the + segment as a continuation of the future. + The segment returns void so the future is (kind of) detached - but this should be done + without the overhead of a future::detach. -/* - TODO: The ergonimics of chains are painful with three arguements. We could reduce to a single - argument or move to a concept? Here I really want the forward reference to be an rvalue ref. + How is cancellation handled here? Let's say we have this: + + `auto f = start(then(future));` - The implementation of sync_wait is complicated by the fact that the promise is currently hard/ - wired into the chain. sync_wait needs to be able to invoke the promise/receiver - _then_ flag - the condition that it is ready. + And we destruct f. We need to _delete_ the (detached) future. Where is this held? f is only + holding the promise. */ +template +inline auto then(F&& future) { + return chain{std::tuple<>{}, + segment{type::result_type>{}, + [_future = std::forward(future)](auto&& f) mutable { + return std::move(_future).then(std::forward(f)); + }}}; +} + +// TODO: (sean-parent) - should we make this pipeable? +// TODO: (sean-parent) - fix case where invoke_t is void. + +template +inline auto start(Chain&& chain, Args&&... args) { + using result_t = Chain::template result_type; + using invoke_t = decltype(std::forward(chain).invoke( + std::declval>(), std::forward(args)...)); + + if constexpr (std::is_same_v) { + auto [receiver, future] = + stlab::package(stlab::immediate_executor, [](auto&& value) { + return std::forward(value); + }); + std::forward(chain).invoke(std::move(receiver), std::forward(args)...); + return std::move(future); + } else { + auto p = std::make_shared>(); + auto [receiver, future] = + stlab::package(stlab::immediate_executor, [p](auto&& value) { + return std::forward(value); + }); + *p = std::forward(chain).invoke(std::move(receiver), std::forward(args)...); + return std::move(future); + } +} + + +template +inline auto sync_wait(Chain&& chain, Args&&... args) { + using result_t = Chain::template result_type; + + struct receiver_t { + std::optional result; + std::exception_ptr error{nullptr}; + std::mutex m; + std::condition_variable cv; + + void operator()(result_t&& value) { + { + std::lock_guard lock(m); + result = std::move(value); + } + cv.notify_one(); + } + + void set_exception(std::exception_ptr p) { + { + std::lock_guard lock(m); + error = p; + } + cv.notify_one(); + } + + bool canceled() const { return false; } + } receiver; -template -inline auto sync_wait(Chain&& chain) { /* - TODO: (sean-parent) - we should have an invoke awaiting parameterized on what we are waiting - The implementation of which would be used in stlab::await() and used here. With this - construct we don't spin up more than one thread (hmm, maybe we shouldn't?). + REVISIT: (sean-parent) - chain invoke doesn't work with std::ref(receiver). We should + fix that but for now create a receiver-ref. */ - auto appended = std::forward(chain) | [&] - invoke_awaiting( - ); -} -#endif -#if 0 -inline auto apply() { - return segment{[](auto&& f, auto&&... args) { - return std::forward(f)(std::forward(args)...); - }}; -} + auto hold = std::forward(chain).invoke(receiver_ref{&receiver}, + std::forward(args)...); -template -inline auto then(F&& future) { - return segment{[_future = std::forward(future)](auto&& f) { - return std::move(_future).then(std::forward(f)); - }}; + std::unique_lock lock(receiver.m); + receiver.cv.wait(lock, [&] { return receiver.result.has_value() || receiver.error; }); + + if (receiver.error) { + std::rethrow_exception(receiver.error); + } + return *receiver.result; } -#endif +/* + TODO: The ergonimics of chains are painful with three arguements. We could reduce to a + single argument or move to a concept? Here I really want the forward reference to be an + rvalue ref. + + The implementation of sync_wait is complicated by the fact that the promise is currently + hard/ wired into the chain. sync_wait needs to be able to invoke the promise/receiver - + _then_ flag the condition that it is ready. +*/ } // namespace chains::inline v1 //-------------------------------------------------------------------------------------------------- + + + #include #include #include @@ -309,11 +428,27 @@ using namespace chains; using namespace stlab; TEST_CASE("Initial draft", "[initial_draft]") { -#if 0 + + GIVEN("A tuple of mixed types") { + auto oneInt2Int = [](int a) { return a * 2; }; + auto twoInt2Int = [](int a, int b) { return a + b; }; + auto void2Int = []() { return 42; }; + + auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; + + auto f = start(std::move(a0), 2); + REQUIRE(f.is_ready()); + auto val = f.get_ready(); + REQUIRE(46 == val); + } + + auto a0 = on(default_executor) | [] { cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; return 42; }; + // std::cout << typeid(decltype(a0)::result_type<>).name() << "\n"; + // auto future = start(std::move(a0)); auto a1 = std::move(a0) | on(default_executor) | [](int x) { cout << "received: " << x << " on thread: " << std::this_thread::get_id() << "\n"; @@ -325,7 +460,7 @@ TEST_CASE("Initial draft", "[initial_draft]") { cout << "Ready to go async!\n"; #if 0 - auto a2 = then(std::move(a1)()) | [](std::string s){ + auto a2 = then(std::move(a1)) | [](std::string s) { cout << s << "<-- \n"; return 0; }; @@ -350,7 +485,12 @@ TEST_CASE("Initial draft", "[initial_draft]") { // std::this_thread::sleep_for(3s); - std::cout << await(std::move(a1)()) << "\n"; -#endif + // std::cout << await(start(std::move(a1))) << "\n"; + + //auto future = start(std::move(a1)); + //auto a2 = then(future) | [](std::string s) { return s + "<-- \n"; }; + + //std::cout << sync_wait(std::move(a2)) << "\n"; + pre_exit(); } diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index bf4853f..9cee3e7 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -42,6 +42,22 @@ TEST_CASE("Test tuple consume", "[tuple_consume]") { REQUIRE((result == std::make_tuple(6, 4.0f))); } } - } + +TEST_CASE("Test concut functions", "[tuple_consume]") { + + GIVEN("A tuple of mixed types") + { + auto oneInt2Int = [](int a) { return a * 2; }; + auto twoInt2Int = [](int a, int b) { return a + b; }; + auto void2Int = []() { return 42; }; + auto functions = std::make_tuple(oneInt2Int, void2Int, twoInt2Int); + + GIVEN("the calculation is done") + { + auto result = chains::calc(functions, 2); + REQUIRE(46 == result); + } + } +} From 813ece3ad40ef3200f925ce2df08b5846a1252f5 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 14 Nov 2025 21:51:53 +0100 Subject: [PATCH 08/16] Working example with * chain of arbitrary callables * cancellation token --- test/initial_draft.cpp | 134 ++++++++++++++++++++++++++++++++--------- 1 file changed, 104 insertions(+), 30 deletions(-) diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index fc8b18a..4b75458 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -63,7 +63,7 @@ class segment { public: /* - An apply operation may inject additional arguments into the segment. The plan is that the + An apply operation may inject additional arguments into the segment. The plan is that the receiver will get sent to apply and this is how cancellation tokens can be injected into an operation. Something like `with_cancellation`. @@ -171,22 +171,19 @@ class chain { template auto expand(R&& receiver) && { - using receiver_t = std::decay_t; - auto shared_receiver = std::make_shared(std::forward(receiver)); - return detail::fold_over( - [shared_receiver]([[maybe_unused]] auto& fold, auto&& first, + [_receiver = std::forward(receiver)]([[maybe_unused]] auto fold, auto&& first, auto&&... rest) mutable { if constexpr (sizeof...(rest) == 0) { - return [shared_receiver, _segment = STLAB_FWD(first).append(shared_receiver)]( - auto&&... args) mutable { - return std::move(_segment).invoke(shared_receiver, STLAB_FWD(args)...); + return [_receiver, _segment = STLAB_FWD(first).append([_receiver](auto&& val) { + _receiver->operator()(std::forward(val)); + })](auto&&... args) mutable { + return std::move(_segment).invoke(_receiver, STLAB_FWD(args)...); }; } else { - return [shared_receiver, - _segment = STLAB_FWD(first).append( - fold(fold, STLAB_FWD(rest)...))](auto&&... args) mutable { - return std::move(_segment).invoke(shared_receiver, STLAB_FWD(args)...); + return [_receiver, _segment = STLAB_FWD(first).append(fold( + fold, STLAB_FWD(rest)...))](auto&&... args) mutable { + return std::move(_segment).invoke(_receiver, STLAB_FWD(args)...); }; } }, @@ -332,26 +329,34 @@ inline auto then(F&& future) { template inline auto start(Chain&& chain, Args&&... args) { - using result_t = Chain::template result_type; - using invoke_t = decltype(std::forward(chain).invoke( - std::declval>(), std::forward(args)...)); + using result_t = typename Chain::template result_type; - if constexpr (std::is_same_v) { - auto [receiver, future] = - stlab::package(stlab::immediate_executor, [](auto&& value) { - return std::forward(value); - }); - std::forward(chain).invoke(std::move(receiver), std::forward(args)...); - return std::move(future); + using package_task_t = decltype(stlab::package( + stlab::immediate_executor, + [](auto&& v) { return std::forward(v); }).first); + + auto shared = std::shared_ptr(); + + // Build the receiver and future first. + auto [receiver, future] = stlab::package( + stlab::immediate_executor, [_shared = shared](auto&& v) { return std::forward(v); }); + + // Promote receiver to shared_ptr to extend lifetime beyond this scope. + shared = std::make_shared(std::move(receiver)); + + // Recompute invoke_t based on passing the shared_ptr (pointer semantics). + using invoke_t = + decltype(std::forward(chain).invoke(shared, std::forward(args)...)); + + if constexpr (std::is_void_v) { + // Just invoke; lifetime of receiver is now owned by captures inside the async chain. + std::forward(chain).invoke(shared, std::forward(args)...); } else { - auto p = std::make_shared>(); - auto [receiver, future] = - stlab::package(stlab::immediate_executor, [p](auto&& value) { - return std::forward(value); - }); - *p = std::forward(chain).invoke(std::move(receiver), std::forward(args)...); - return std::move(future); + // Keep any handle the chain returns (e.g. continuation future or cancellation handle). + auto hold = std::forward(chain).invoke(shared, std::forward(args)...); + (void)hold; // store or return if you later need it } + return std::move(future); } @@ -402,7 +407,7 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { } /* - TODO: The ergonimics of chains are painful with three arguements. We could reduce to a + TODO: The ergonomics of chains are painful with three arguments. We could reduce to a single argument or move to a concept? Here I really want the forward reference to be an rvalue ref. @@ -427,6 +432,75 @@ using namespace std; using namespace chains; using namespace stlab; +// Cancellation example + +struct cancellation_source { + struct state { + std::atomic_bool canceled{false}; + }; + std::shared_ptr _s = std::make_shared(); + void cancel() const { _s->canceled.store(true, std::memory_order_relaxed); } +}; + +struct cancellation_token { + std::shared_ptr _s; + bool canceled() const { return _s->canceled.load(std::memory_order_relaxed); } +}; + +// Segment that injects a cancellation_token (Injects != void) +inline auto with_cancellation(cancellation_source src) { + return chains::segment{chains::type{}, + [_src = std::move(src)](auto&& f, auto&&... args) mutable { + // Create token and forward it as first argument + cancellation_token tok{_src._s}; + std::forward(f)(tok, + std::forward(args)...); + }}; +} + +// executor variant that also injects the token and schedules asynchronously +template +auto on_with_cancellation(E&& executor, cancellation_source source) { + return chains::segment{ + chains::type{}, + [_executor = std::forward(executor), _source = std::move(source)](auto&& f, + auto&&... args) mutable { + cancellation_token token{_source._s}; + std::move(_executor)( + [_f = std::forward(f), _token = token, + _args = std::tuple{std::forward(args)...}]() mutable noexcept { + std::apply( + [&_f, &_token](auto&&... as) { + std::forward(_f)(_token, std::forward(as)...); + }, + std::move(_args)); + }); + }}; +} + +TEST_CASE("Cancellation injection", "[initial_draft]") { + cancellation_source src; + + // Build a chain where each function expects the token as first argument. + // First function uses the token, returns an int. + auto c = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 2; + } | [](int y) { return y + 10; }; // token only needed by first step + + auto f = start(std::move(c), 5); + REQUIRE(f.get_ready() == 20); // (5*2)+10 + + // Demonstrate cancel before start + src.cancel(); + auto c2 = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 3; + }; + auto f2 = start(std::move(c2), 7); + REQUIRE(f2.get_ready() == 0); +} + TEST_CASE("Initial draft", "[initial_draft]") { GIVEN("A tuple of mixed types") { From b1b1f3820558088379f9e03f542243870890b0a6 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 14 Nov 2025 22:29:32 +0100 Subject: [PATCH 09/16] Cleanup --- test/initial_draft.cpp | 183 +++++++++++++++++++++-------------------- test/tuple_tests.cpp | 2 +- 2 files changed, 94 insertions(+), 91 deletions(-) diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 4b75458..1034110 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -8,8 +8,6 @@ #include -// temporary -#include #include #include @@ -17,7 +15,7 @@ /* -If exception inside of a segment _apply_ function throws an exception then the exception must be +If exception inside a segment _apply_ function throws an exception then the exception must be set on the receiver. */ @@ -26,12 +24,8 @@ namespace chains::inline v1 { /* segment is invoked with a receiver - - - - */ - template struct receiver_ref { Receiver* _receiver; @@ -63,7 +57,7 @@ class segment { public: /* - An apply operation may inject additional arguments into the segment. The plan is that the + An apply operation may inject additional arguments into the segment. The plan is that the receiver will get sent to apply and this is how cancellation tokens can be injected into an operation. Something like `with_cancellation`. @@ -105,9 +99,9 @@ class segment { /* The apply function for a segment always returns void. - Invoke will check the receiver for cancelation - - If not conceled, apply(segement), cancelation is checked before execution of the segment - and any exception during the segment is propogated to the receiever. + Invoke will check the receiver for cancellation - + If not canceled, apply(segment), cancellation is checked before execution of the segment + and any exception during the segment is propagated to the receiver. */ template @@ -116,10 +110,10 @@ class segment { // if (receiver.canceled()) return; return std::move(_apply)( [_f = tuple_compose_greedy(std::move(_functions)), - _receiver = std::forward(receiver)](auto&&... args) mutable noexcept { + _receiver = std::forward(receiver)](T&&... args) mutable noexcept { if (_receiver->canceled()) return; try { - std::move(_f)(std::forward(args)...); + std::move(_f)(std::forward(args)...); } catch (...) { _receiver->set_exception(std::current_exception()); } @@ -154,7 +148,7 @@ class chain { static consteval auto result_type_helper(Tail&& tail, segment&& head) { return detail::fold_over( - []([[maybe_unused]]auto fold, auto&& first, auto&&... rest) { + []([[maybe_unused]] auto fold, auto&& first, auto&&... rest) { if constexpr (sizeof...(rest) == 0) { return [_segment = STLAB_FWD(first)](auto&&... args) mutable { return std::move(_segment).result_type_helper(STLAB_FWD(args)...); @@ -175,15 +169,17 @@ class chain { [_receiver = std::forward(receiver)]([[maybe_unused]] auto fold, auto&& first, auto&&... rest) mutable { if constexpr (sizeof...(rest) == 0) { - return [_receiver, _segment = STLAB_FWD(first).append([_receiver](auto&& val) { - _receiver->operator()(std::forward(val)); - })](auto&&... args) mutable { - return std::move(_segment).invoke(_receiver, STLAB_FWD(args)...); + return [_receiver, + _segment = STLAB_FWD(first).append([_receiver](V&& val) { + _receiver->operator()(std::forward(val)); + })](T&&... args) mutable { + return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } else { - return [_receiver, _segment = STLAB_FWD(first).append(fold( - fold, STLAB_FWD(rest)...))](auto&&... args) mutable { - return std::move(_segment).invoke(_receiver, STLAB_FWD(args)...); + return [_receiver, _segment = STLAB_FWD(first).append( + fold(fold, STLAB_FWD(rest)...))]( + T&&... args) mutable { + return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } }, @@ -268,7 +264,7 @@ chain(Tail&& tail, segment&& head) -> chain; template -inline auto operator|(segment&& head, F&& f) { +auto operator|(segment&& head, F&& f) { return chain{std::tuple<>{}, std::move(head).append(std::forward(f))}; } @@ -288,16 +284,17 @@ last item in the chain as a segment. */ template -inline auto on(E&& executor) { - return segment{ - type{}, [_executor = std::forward(executor)](auto&& f, auto&&... args) mutable { - std::move(_executor)( - [_f = std::forward(f), - _args = std::tuple{std::forward(args)...}]() mutable noexcept { - std::apply(std::move(_f), std::move(_args)); - }); - // return std::monostate{}; - }}; +auto on(E&& executor) { + return segment{type{}, + [_executor = std::forward(executor)]( + F&& f, Args&&... args) mutable { + std::move(_executor)( + [_f = std::forward(f), + _args = std::tuple{std::forward(args)...}]() mutable noexcept { + std::apply(std::move(_f), std::move(_args)); + }); + // return std::monostate{}; + }}; } /* @@ -305,7 +302,7 @@ inline auto on(E&& executor) { segment as a continuation of the future. The segment returns void so the future is (kind of) detached - but this should be done - without the overhead of a future::detach. + without the overhead of a future::detach. How is cancellation handled here? Let's say we have this: @@ -316,7 +313,7 @@ inline auto on(E&& executor) { */ template -inline auto then(F&& future) { +auto then(F&& future) { return chain{std::tuple<>{}, segment{type::result_type>{}, [_future = std::forward(future)](auto&& f) mutable { @@ -328,18 +325,17 @@ inline auto then(F&& future) { // TODO: (sean-parent) - fix case where invoke_t is void. template -inline auto start(Chain&& chain, Args&&... args) { +auto start(Chain&& chain, Args&&... args) { using result_t = typename Chain::template result_type; - using package_task_t = decltype(stlab::package( - stlab::immediate_executor, - [](auto&& v) { return std::forward(v); }).first); - + using package_task_t = stlab::packaged_task; auto shared = std::shared_ptr(); // Build the receiver and future first. - auto [receiver, future] = stlab::package( - stlab::immediate_executor, [_shared = shared](auto&& v) { return std::forward(v); }); + auto [receiver, future] = + stlab::package(stlab::immediate_executor, [_shared = shared](T&& val) { + return std::forward(val); + }); // Promote receiver to shared_ptr to extend lifetime beyond this scope. shared = std::make_shared(std::move(receiver)); @@ -359,10 +355,9 @@ inline auto start(Chain&& chain, Args&&... args) { return std::move(future); } - template inline auto sync_wait(Chain&& chain, Args&&... args) { - using result_t = Chain::template result_type; + using result_t = typename Chain::template result_type; struct receiver_t { std::optional result; @@ -372,7 +367,7 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { void operator()(result_t&& value) { { - std::lock_guard lock(m); + std::lock_guard lock(m); result = std::move(value); } cv.notify_one(); @@ -380,7 +375,7 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { void set_exception(std::exception_ptr p) { { - std::lock_guard lock(m); + std::lock_guard lock(m); error = p; } cv.notify_one(); @@ -390,7 +385,7 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { } receiver; /* - REVISIT: (sean-parent) - chain invoke doesn't work with std::ref(receiver). We should + REVISIT: (sean-parent) - chain invoke doesn't work with std::ref(receiver). We should fix that but for now create a receiver-ref. */ @@ -420,9 +415,6 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { //-------------------------------------------------------------------------------------------------- - - - #include #include #include @@ -438,23 +430,22 @@ struct cancellation_source { struct state { std::atomic_bool canceled{false}; }; - std::shared_ptr _s = std::make_shared(); - void cancel() const { _s->canceled.store(true, std::memory_order_relaxed); } + std::shared_ptr _state = std::make_shared(); + void cancel() const { _state->canceled.store(true, std::memory_order_relaxed); } }; struct cancellation_token { - std::shared_ptr _s; - bool canceled() const { return _s->canceled.load(std::memory_order_relaxed); } + std::shared_ptr _state; + bool canceled() const { return _state->canceled.load(std::memory_order_relaxed); } }; // Segment that injects a cancellation_token (Injects != void) inline auto with_cancellation(cancellation_source src) { return chains::segment{chains::type{}, - [_src = std::move(src)](auto&& f, auto&&... args) mutable { + [_src = std::move(src)](F&& f, Args&&... args) mutable { // Create token and forward it as first argument - cancellation_token tok{_src._s}; - std::forward(f)(tok, - std::forward(args)...); + cancellation_token token{_src._state}; + std::forward(f)(token, std::forward(args)...); }}; } @@ -463,15 +454,15 @@ template auto on_with_cancellation(E&& executor, cancellation_source source) { return chains::segment{ chains::type{}, - [_executor = std::forward(executor), _source = std::move(source)](auto&& f, - auto&&... args) mutable { - cancellation_token token{_source._s}; + [_executor = std::forward(executor), + _source = std::move(source)](F&& f, Args&&... args) mutable { + cancellation_token token{_source._state}; std::move(_executor)( - [_f = std::forward(f), _token = token, - _args = std::tuple{std::forward(args)...}]() mutable noexcept { + [_f = std::forward(f), _token = token, + _args = std::make_tuple(std::forward(args)...)]() mutable noexcept { std::apply( - [&_f, &_token](auto&&... as) { - std::forward(_f)(_token, std::forward(as)...); + [&_f, &_token](As&&... as) { + std::forward(_f)(_token, std::forward(as)...); }, std::move(_args)); }); @@ -479,31 +470,44 @@ auto on_with_cancellation(E&& executor, cancellation_source source) { } TEST_CASE("Cancellation injection", "[initial_draft]") { - cancellation_source src; - - // Build a chain where each function expects the token as first argument. - // First function uses the token, returns an int. - auto c = with_cancellation(src) | [](cancellation_token token, int x) { - if (token.canceled()) return 0; - return x * 2; - } | [](int y) { return y + 10; }; // token only needed by first step - - auto f = start(std::move(c), 5); - REQUIRE(f.get_ready() == 20); // (5*2)+10 - - // Demonstrate cancel before start - src.cancel(); - auto c2 = with_cancellation(src) | [](cancellation_token token, int x) { - if (token.canceled()) return 0; - return x * 3; - }; - auto f2 = start(std::move(c2), 7); - REQUIRE(f2.get_ready() == 0); + { + cancellation_source src; + + // Build a chain where the first function expects the token as first argument. + auto c = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 2; + } | [](int y) { return y + 10; }; // token only needed by first step + + auto f = start(std::move(c), 5); + REQUIRE(f.get_ready() == 20); // (5*2)+10 + + // Demonstrate cancel before start + src.cancel(); + auto c2 = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 3; + }; + auto f2 = start(std::move(c2), 7); + REQUIRE(f2.get_ready() == 0); + } + //{ + // cancellation_source src; + + // // Build a chain where each function expects the token as first argument. + // // First function uses the token, returns an int. + // auto c = with_cancellation(src) | [](cancellation_token token, int x) { + // if (token.canceled()) return 0; + // return x * 2; + // } | [](int y) { return y + 10; }; // token only needed by first step + + // auto f = start(std::move(c), 5); + // REQUIRE(f.get_ready() == 20); // (5*2)+10 + //} } TEST_CASE("Initial draft", "[initial_draft]") { - - GIVEN("A tuple of mixed types") { + GIVEN("a sequence of callables with different arguments") { auto oneInt2Int = [](int a) { return a * 2; }; auto twoInt2Int = [](int a, int b) { return a + b; }; auto void2Int = []() { return 42; }; @@ -516,7 +520,6 @@ TEST_CASE("Initial draft", "[initial_draft]") { REQUIRE(46 == val); } - auto a0 = on(default_executor) | [] { cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; return 42; @@ -561,10 +564,10 @@ TEST_CASE("Initial draft", "[initial_draft]") { // std::cout << await(start(std::move(a1))) << "\n"; - //auto future = start(std::move(a1)); - //auto a2 = then(future) | [](std::string s) { return s + "<-- \n"; }; + // auto future = start(std::move(a1)); + // auto a2 = then(future) | [](std::string s) { return s + "<-- \n"; }; - //std::cout << sync_wait(std::move(a2)) << "\n"; + // std::cout << sync_wait(std::move(a2)) << "\n"; pre_exit(); } diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index 9cee3e7..58015e0 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -45,7 +45,7 @@ TEST_CASE("Test tuple consume", "[tuple_consume]") { } -TEST_CASE("Test concut functions", "[tuple_consume]") { +TEST_CASE("Test concat functions", "[tuple_consume]") { GIVEN("A tuple of mixed types") { From aca7e237fed52b72aa53ec81fe620ead4cf30fe2 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Sat, 15 Nov 2025 10:25:39 +0100 Subject: [PATCH 10/16] Improve calc function --- include/chains/tuple.hpp | 25 +++++++++++++------------ test/initial_draft.cpp | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 42d8eeb..4da0d8f 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -140,24 +140,25 @@ constexpr auto tuple_consume(Tuple&& values) { }; } -template -struct calculator { - template - static constexpr auto apply(F& f, T t) { - if constexpr (I == std::tuple_size_v) { - return std::get<0>(t); - } else { - return calculator::apply(f, chains::tuple_consume(std::move(t))(std::get(f))); - } +template +constexpr auto calc_step(F& f, T t) { + if constexpr (I == std::tuple_size_v) { + return std::get<0>(t); + } else { + auto&& fn = std::get(f); + auto next = chains::tuple_consume(std::move(t))(fn); + return calc_step(f, std::move(next)); } -}; +} template constexpr auto calc(F f, Args&&... args) { - auto arguments = std::make_tuple(std::forward(args)...); - return calculator<0>::apply(f, arguments); + return calc_step<0>( + f, std::make_tuple(std::forward(args)...) + ); } + template constexpr auto tuple_compose_greedy(std::tuple&& sequence) { return [_sequence = std::move(sequence)](auto&&... args) mutable { diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 1034110..883bade 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -519,7 +519,7 @@ TEST_CASE("Initial draft", "[initial_draft]") { auto val = f.get_ready(); REQUIRE(46 == val); } - +#if 0 auto a0 = on(default_executor) | [] { cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; return 42; @@ -535,7 +535,7 @@ TEST_CASE("Initial draft", "[initial_draft]") { cout << "Main thread: " << std::this_thread::get_id() << "\n"; cout << "Ready to go async!\n"; - +#endif #if 0 auto a2 = then(std::move(a1)) | [](std::string s) { cout << s << "<-- \n"; From b6c272095d8c4942002282f79fab53e02a85678d Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Sat, 15 Nov 2025 10:50:22 +0100 Subject: [PATCH 11/16] Fixing move-only issues --- include/chains/tuple.hpp | 18 +++++++++--------- test/initial_draft.cpp | 20 +++++++++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 4da0d8f..898ad5a 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -40,7 +40,7 @@ auto operator|(tuple_pipeable&& p, F& f) { /* Check if F is invocable with first K elements of tuple T */ template constexpr auto invocable_with_prefix(std::index_sequence) { - return requires(F&& f, T& tup) { std::invoke(f, std::get(tup)...); }; + return requires(F&& f, T&& tup) { std::invoke(f, std::move(std::get(tup))...); }; } /* Find largest prefix size (0..N) for which F is invocable */ @@ -59,7 +59,7 @@ struct find_max_prefix { /* Invoke F with first K elements of tuple t (K known at compile time) */ template -constexpr auto invoke_prefix(F&& f, Tuple& t) { +constexpr auto invoke_prefix(F&& f, Tuple&& t) { if constexpr (K == 0) { // No arguments: only attempt if callable with () if constexpr (requires(F&& f2) { std::invoke(f2); }) { @@ -74,11 +74,11 @@ constexpr auto invoke_prefix(F&& f, Tuple& t) { } } else { return [&](std::index_sequence) { - if constexpr (std::is_void_v(t)...))>) { - std::invoke(f, std::get(t)...); + if constexpr (std::is_void_v(t))...))>) { + std::invoke(f, std::move(std::get(t))...); return std::monostate{}; } else { - return std::invoke(f, std::get(t)...); + return std::invoke(f, std::move(std::get(t))...); } }(std::make_index_sequence{}); } @@ -107,7 +107,7 @@ auto tuple_compose(std::tuple&& sequence) { /* Construct tuple tail starting at Offset (compile time) */ template -constexpr auto tuple_tail_at(Tuple& t, std::index_sequence) { +constexpr auto tuple_tail_at(Tuple&& t, std::index_sequence) { return std::tuple{std::move(std::get(t))...}; } @@ -131,10 +131,10 @@ constexpr auto tuple_consume(Tuple&& values) { if constexpr (consumed == 0) { // Remaining is original tuple (no elements consumed) - return std::tuple_cat(std::make_tuple(std::move(result)), _values); + return std::tuple_cat(std::make_tuple(std::move(result)), std::move(_values)); } else { auto remaining = - tuple_tail_at(_values, std::make_index_sequence{}); + tuple_tail_at(std::move(_values), std::make_index_sequence{}); return std::tuple_cat(std::make_tuple(std::move(result)), std::move(remaining)); } }; @@ -143,7 +143,7 @@ constexpr auto tuple_consume(Tuple&& values) { template constexpr auto calc_step(F& f, T t) { if constexpr (I == std::tuple_size_v) { - return std::get<0>(t); + return std::get<0>(std::move(t)); } else { auto&& fn = std::get(f); auto next = chains::tuple_consume(std::move(t))(fn); diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 883bade..6862d17 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -418,6 +418,7 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { #include #include #include +#include #include using namespace std; @@ -469,6 +470,7 @@ auto on_with_cancellation(E&& executor, cancellation_source source) { }}; } + TEST_CASE("Cancellation injection", "[initial_draft]") { { cancellation_source src; @@ -491,6 +493,7 @@ TEST_CASE("Cancellation injection", "[initial_draft]") { auto f2 = start(std::move(c2), 7); REQUIRE(f2.get_ready() == 0); } + //{ // cancellation_source src; @@ -519,6 +522,21 @@ TEST_CASE("Initial draft", "[initial_draft]") { auto val = f.get_ready(); REQUIRE(46 == val); } + + GIVEN("a sequence of callables that just work with move only value") { + auto oneInt2Int = [](move_only a) { return move_only(a.member() * 2); }; + auto twoInt2Int = [](move_only a, move_only b) { return move_only(a.member() + b.member()); }; + auto void2Int = []() { return move_only(42); }; + + auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; + + auto f = start(std::move(a0), move_only(2)); + REQUIRE(f.is_ready()); + auto val = std::move(f).get_ready(); + REQUIRE(46 == val.member()); + } + + #if 0 auto a0 = on(default_executor) | [] { cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; @@ -569,5 +587,5 @@ TEST_CASE("Initial draft", "[initial_draft]") { // std::cout << sync_wait(std::move(a2)) << "\n"; - pre_exit(); + //pre_exit(); } From c2e54d1e0ad51dda6e546346ce555f74af2b5424 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Sat, 15 Nov 2025 16:18:02 +0100 Subject: [PATCH 12/16] Draft of split --- include/chains/tuple.hpp | 11 ++- test/initial_draft.cpp | 165 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 161 insertions(+), 15 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 898ad5a..0d9b94a 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -140,10 +140,17 @@ constexpr auto tuple_consume(Tuple&& values) { }; } -template +template constexpr auto calc_step(F& f, T t) { if constexpr (I == std::tuple_size_v) { - return std::get<0>(std::move(t)); + // Base case: we finished applying all functions. + // If there are no remaining tuple elements, return std::monostate + // (mirrors void -> monostate mapping elsewhere). + if constexpr (std::tuple_size_v == 0) { + return std::monostate{}; + } else { + return std::get<0>(std::move(t)); + } } else { auto&& fn = std::get(f); auto next = chains::tuple_consume(std::move(t))(fn); diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 6862d17..beb7b8b 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -332,10 +332,9 @@ auto start(Chain&& chain, Args&&... args) { auto shared = std::shared_ptr(); // Build the receiver and future first. - auto [receiver, future] = - stlab::package(stlab::immediate_executor, [_shared = shared](T&& val) { - return std::forward(val); - }); + auto [receiver, future] = stlab::package( + stlab::immediate_executor, + [_shared = shared](T&& val) { return std::forward(val); }); // Promote receiver to shared_ptr to extend lifetime beyond this scope. shared = std::make_shared(std::move(receiver)); @@ -442,12 +441,13 @@ struct cancellation_token { // Segment that injects a cancellation_token (Injects != void) inline auto with_cancellation(cancellation_source src) { - return chains::segment{chains::type{}, - [_src = std::move(src)](F&& f, Args&&... args) mutable { - // Create token and forward it as first argument - cancellation_token token{_src._state}; - std::forward(f)(token, std::forward(args)...); - }}; + return chains::segment{ + chains::type{}, + [_src = std::move(src)](F&& f, Args&&... args) mutable { + // Create token and forward it as first argument + cancellation_token token{_src._state}; + std::forward(f)(token, std::forward(args)...); + }}; } // executor variant that also injects the token and schedules asynchronously @@ -470,6 +470,131 @@ auto on_with_cancellation(E&& executor, cancellation_source source) { }}; } +template + requires std::is_copy_constructible_v +struct split_state { + std::mutex _m; + std::condition_variable _cv; + std::optional _value; + std::exception_ptr _error{nullptr}; + std::vector> _continuations; + std::atomic_bool _started{false}; + std::atomic_bool _completed{false}; + + void set_value(T v) { + if (_completed.load(std::memory_order_acquire)) return; + { + std::lock_guard lk(_m); + _value.emplace(std::move(v)); + _completed.store(true, std::memory_order_release); + } + auto continuations = extract_continuations(); + for (auto& c : continuations) + c(*_value); + _cv.notify_all(); + } + + void set_exception(std::exception_ptr p) { + { + std::lock_guard lk(_m); + _error = p; + _completed.store(true, std::memory_order_release); + } + // auto continuations = extract_continuations(); + // for (auto& c : continuations) { + // // We skip invoking branch continuations on exception; if needed they can be + // // generalized to receive exception too. + // } + _cv.notify_all(); + } + + void add_continuation(std::function fn) { + std::unique_lock lk(_m); + if (_completed.load(std::memory_order_acquire) && _value) { + auto v = *_value; // copy out + lk.unlock(); + fn(v); + return; + } + _continuations.push_back(std::move(fn)); + } + +private: + std::vector> extract_continuations() { + std::vector> tmp; + std::swap(tmp, _continuations); + return tmp; + } +}; + +template +struct split_holder { + std::shared_ptr _upstream; + std::shared_ptr _state; + std::once_flag _init_once; + + explicit split_holder(Upstream&& u) : _upstream(std::make_shared(std::move(u))) {} + +private: + template + auto make_branch(F&& f) { + using upstream_t = Upstream; + + auto branch_segment = segment{ + type{}, + [this](Composed&& composed, + StartArgs&&... start_args) mutable { + using result_t = typename upstream_t::template result_type; + + // Allocate shared state only once (first branch start) + std::call_once(_init_once, + [this] { _state = std::make_shared>(); }); + + auto state = std::static_pointer_cast>(_state); + + // Register this branch's continuation + state->add_continuation( + [comp = std::forward(composed)](const result_t& v) mutable noexcept { + try { + comp(v); + } catch (...) { /* optional: log */ + } + }); + + // Start upstream only once + if (!state->_started.exchange(true, std::memory_order_acq_rel)) { + struct upstream_receiver { + std::shared_ptr> _s; + void operator()(result_t&& val) { _s->set_value(std::move(val)); } + void set_exception(std::exception_ptr p) { _s->set_exception(p); } + bool canceled() const { return false; } + }; + auto receiver = std::make_shared(); + receiver->_s = state; + // Move upstream here, because chain wants an rvalue and we only start once. + std::move(*_upstream).invoke(receiver, std::forward(start_args)...); + } + }, + std::forward(f)}; + + return chain{std::tuple<>{}, std::move(branch_segment)}; + } + +public: + template + auto fan(F&& f) & { + return make_branch(std::forward(f)); + } + template + auto fan(F&& f) && { + return make_branch(std::forward(f)); + } +}; + +template +auto split(Chain&& c) { + return split_holder{std::forward(c)}; +} TEST_CASE("Cancellation injection", "[initial_draft]") { { @@ -509,6 +634,19 @@ TEST_CASE("Cancellation injection", "[initial_draft]") { //} } +// --- Example test demonstrating split --------------------------------------------------------- +TEST_CASE("Split fan-out", "[initial_draft]") { + auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; + auto splitter = split(std::move(base)); + auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; + auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); + + auto f_right = start(std::move(right), 10); + auto f_left = start(std::move(left), 5); + REQUIRE(f_right.get_ready() == std::string("v=15")); + REQUIRE(f_left.get_ready() == 31); +} + TEST_CASE("Initial draft", "[initial_draft]") { GIVEN("a sequence of callables with different arguments") { auto oneInt2Int = [](int a) { return a * 2; }; @@ -525,7 +663,9 @@ TEST_CASE("Initial draft", "[initial_draft]") { GIVEN("a sequence of callables that just work with move only value") { auto oneInt2Int = [](move_only a) { return move_only(a.member() * 2); }; - auto twoInt2Int = [](move_only a, move_only b) { return move_only(a.member() + b.member()); }; + auto twoInt2Int = [](move_only a, move_only b) { + return move_only(a.member() + b.member()); + }; auto void2Int = []() { return move_only(42); }; auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; @@ -536,7 +676,6 @@ TEST_CASE("Initial draft", "[initial_draft]") { REQUIRE(46 == val.member()); } - #if 0 auto a0 = on(default_executor) | [] { cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; @@ -587,5 +726,5 @@ TEST_CASE("Initial draft", "[initial_draft]") { // std::cout << sync_wait(std::move(a2)) << "\n"; - //pre_exit(); + // pre_exit(); } From 4c6908f86ca34c538ccd15ed9fb5ee57eafce37f Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Sat, 15 Nov 2025 17:52:10 +0100 Subject: [PATCH 13/16] Added bounded split --- test/initial_draft.cpp | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index beb7b8b..7e2d9d3 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -596,6 +596,79 @@ auto split(Chain&& c) { return split_holder{std::forward(c)}; } +template +struct split_holder_bound { + using upstream_result_t = typename Upstream::template result_type; + + std::shared_ptr _upstream; + std::tuple...> _bound_args; + std::shared_ptr> _state; + std::once_flag _start_once; + + explicit split_holder_bound(Upstream&& u, BoundArgs&&... args) + : _upstream(std::make_shared(std::move(u))), + _bound_args(std::forward(args)...), + _state(std::make_shared>()) {} + +private: + template + auto make_branch(F&& f) { + // Segment injects upstream_result_t so result_type<> with () sees correct type. + auto branch_segment = segment{ + type{}, + [this](Composed&& composed) mutable { + // Register branch continuation (called after upstream completes) + _state->add_continuation( + [comp = std::forward(composed)](const upstream_result_t& v) mutable { + try { + comp(v); + } catch (...) { /* optional branch error handling */ + } + }); + + // Start upstream only once + if (!_state->_started.exchange(true, std::memory_order_acq_rel)) { + struct upstream_receiver { + std::shared_ptr> _s; + void operator()(upstream_result_t&& val) { _s->set_value(std::move(val)); } + void set_exception(std::exception_ptr p) { _s->set_exception(p); } + bool canceled() const { return false; } + }; + auto receiver = std::make_shared(); + receiver->_s = _state; + + // Invoke upstream with bound arguments (no external start args) + std::apply( + [this, &receiver](auto&... args) { + std::move(*_upstream).invoke(receiver, args...); + }, + _bound_args); + } + }, + std::forward(f) // branch function (receives upstream_result_t injected as first arg) + }; + + return chain{std::tuple<>{}, std::move(branch_segment)}; + } + +public: + template + auto fan(F&& f) & { + return make_branch(std::forward(f)); + } + template + auto fan(F&& f) && { + return make_branch(std::forward(f)); + } +}; + +// Helper to build a bound split holder +template +auto split_bind(Chain&& c, Args&&... args) { + return split_holder_bound, std::decay_t...>{ + std::forward(c), std::forward(args)...}; +} + TEST_CASE("Cancellation injection", "[initial_draft]") { { cancellation_source src; @@ -647,6 +720,23 @@ TEST_CASE("Split fan-out", "[initial_draft]") { REQUIRE(f_left.get_ready() == 31); } +TEST_CASE("Split fan-out bound", "[initial_draft]") { + auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; + + // Bind upstream start argument 10 once: + auto splitter = split_bind(std::move(base), 10); + + // Branches now start with no args; upstream result (15) is injected. + auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; + auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); + + auto f_right = start(std::move(right)); // no argument + auto f_left = start(std::move(left)); // no argument + + REQUIRE(f_right.get_ready() == std::string("v=15")); + REQUIRE(f_left.get_ready() == 31); +} + TEST_CASE("Initial draft", "[initial_draft]") { GIVEN("a sequence of callables with different arguments") { auto oneInt2Int = [](int a) { return a * 2; }; From 16607db9f722786322c8d249dd36d5b03c180ec9 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Sun, 16 Nov 2025 13:10:34 +0100 Subject: [PATCH 14/16] Getting sync_wait example working Move pre_exit() into main to avoid runtime issues of double call Cleanup template code by using lambda with explicit type names --- include/chains/tuple.hpp | 46 ++++++-------- test/CMakeLists.txt | 2 +- test/initial_draft.cpp | 128 +++++++++++++++------------------------ test/main.cpp | 13 ++++ 4 files changed, 83 insertions(+), 106 deletions(-) create mode 100644 test/main.cpp diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 0d9b94a..dd7c622 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -1,9 +1,9 @@ +#include // std::size_t +#include // std::invoke #include // std::tuple, std::get, std::apply, std::tuple_size_v #include // std::is_same_v, std::decay_t, std::is_void_v #include // std::forward, std::move, std::index_sequence, std::make_index_sequence #include // std::monostate -#include // std::invoke -#include // std::size_t #ifndef CHAIN_TUPLE_HPP #define CHAIN_TUPLE_HPP @@ -14,14 +14,13 @@ namespace detail { /* Map void return to std::monostate */ template -inline auto void_to_monostate(F& f) { - return [&_f = f](auto&&... args) mutable { - if constexpr (std::is_same_v< - decltype(std::move(_f)(std::forward(args)...)), void>) { - std::move(_f)(std::forward(args)...); +auto void_to_monostate(F& f) { + return [&_f = f](Args&&... args) mutable { + if constexpr (std::is_same_v(args)...)), void>) { + std::move(_f)(std::forward(args)...); return std::monostate{}; } else { - return std::move(_f)(std::forward(args)...); + return std::move(_f)(std::forward(args)...); } }; } @@ -47,9 +46,9 @@ constexpr auto invocable_with_prefix(std::index_sequence) { template struct find_max_prefix { static constexpr std::size_t value = - invocable_with_prefix(std::make_index_sequence{}) - ? N - : find_max_prefix::value; + invocable_with_prefix(std::make_index_sequence{}) ? + N : + find_max_prefix::value; }; template @@ -84,15 +83,14 @@ constexpr auto invoke_prefix(F&& f, Tuple&& t) { } } - } // namespace detail //-------------------------------------------------------------------------------------------------- template auto tuple_compose(std::tuple&& sequence) { - return [_sequence = std::move(sequence)](auto&&... args) mutable { + return [_sequence = std::move(sequence)](Args&&... args) mutable { return std::move(std::apply( - [_args = std::forward_as_tuple(std::forward(args)...)]( + [_args = std::forward_as_tuple(std::forward(args)...)]( auto& first, auto&... functions) mutable { return ( detail::tuple_pipeable{std::apply(first, std::move(_args))} | @@ -121,20 +119,19 @@ constexpr auto tuple_tail_at(Tuple&& t, std::index_sequence) { */ template constexpr auto tuple_consume(Tuple&& values) { - return [_values = std::forward(values)](auto&& f) mutable { + return [_values = std::forward(values)](F&& f) mutable { using tuple_t = std::decay_t; constexpr std::size_t N = std::tuple_size_v; - using F_t = decltype(f); - constexpr std::size_t consumed = detail::find_max_prefix::value; - auto result = detail::invoke_prefix(std::forward(f), _values); + constexpr std::size_t consumed = detail::find_max_prefix::value; + auto result = detail::invoke_prefix(std::forward(f), _values); if constexpr (consumed == 0) { // Remaining is original tuple (no elements consumed) return std::tuple_cat(std::make_tuple(std::move(result)), std::move(_values)); } else { - auto remaining = - tuple_tail_at(std::move(_values), std::make_index_sequence{}); + auto remaining = tuple_tail_at( + std::move(_values), std::make_index_sequence{}); return std::tuple_cat(std::make_tuple(std::move(result)), std::move(remaining)); } }; @@ -160,16 +157,13 @@ constexpr auto calc_step(F& f, T t) { template constexpr auto calc(F f, Args&&... args) { - return calc_step<0>( - f, std::make_tuple(std::forward(args)...) - ); + return calc_step<0>(f, std::make_tuple(std::forward(args)...)); } - template constexpr auto tuple_compose_greedy(std::tuple&& sequence) { - return [_sequence = std::move(sequence)](auto&&... args) mutable { - return calc(std::move(_sequence), std::forward(args)...); + return [_sequence = std::move(sequence)](Args&&... args) mutable { + return calc(std::move(_sequence), std::forward(args)...); }; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 281eb03..a5e428e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,7 +29,7 @@ add_test(NAME cli.has_help COMMAND intro --help) add_test(NAME cli.version_matches COMMAND intro --version) set_tests_properties(cli.version_matches PROPERTIES PASS_REGULAR_EXPRESSION "${PROJECT_VERSION}") -add_executable(tests tuple_tests.cpp initial_draft.cpp) +add_executable(tests main.cpp tuple_tests.cpp initial_draft.cpp) target_link_libraries( tests PRIVATE chains::chains_warnings diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 7e2d9d3..54720f4 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -3,14 +3,13 @@ #include -#include -#include - -#include - #include #include +#include +#include +#include + #define STLAB_FWD(x) std::forward(x) /* @@ -78,7 +77,7 @@ class segment { The basic operations should follow those from C++ lambdas, for now default everything. and see if the compiler gets it correct. */ - explicit segment(const segment&) = default; + segment(const segment&) = default; segment(segment&&) noexcept = default; segment& operator=(const segment&) = default; segment& operator=(segment&&) noexcept = default; @@ -127,8 +126,11 @@ namespace detail { /// Apply a recursive lambda to each element in the tuple-like Segments. template constexpr auto fold_over(Fold fold, Segments&& segments) { - return std::apply([fold](auto&&... links) mutable { return fold(fold, STLAB_FWD(links)...); }, - STLAB_FWD(segments)); + return std::apply( + [fold](Links&&... links) mutable { + return fold(fold, std::forward(links)...); + }, + STLAB_FWD(segments)); } } // namespace detail @@ -148,36 +150,39 @@ class chain { static consteval auto result_type_helper(Tail&& tail, segment&& head) { return detail::fold_over( - []([[maybe_unused]] auto fold, auto&& first, auto&&... rest) { + []([[maybe_unused]] Fold fold, + First&& first, Rest&&... rest) { if constexpr (sizeof...(rest) == 0) { - return [_segment = STLAB_FWD(first)](auto&&... args) mutable { - return std::move(_segment).result_type_helper(STLAB_FWD(args)...); + return [_segment = std::forward(first)]( + Args&&... args) mutable { + return std::move(_segment).result_type_helper(std::forward(args)...); }; } else { - return [_segment = STLAB_FWD(first).append(fold(fold, STLAB_FWD(rest)...))]( - auto&&... args) mutable { - return std::move(_segment).result_type_helper(STLAB_FWD(args)...); + return [_segment = std::forward(first).append( + fold(fold, std::forward(rest)...))]( + Args&&... args) mutable { + return std::move(_segment).result_type_helper(std::forward(args)...); }; } }, - std::tuple_cat(STLAB_FWD(tail), std::tuple{STLAB_FWD(head)})); + std::tuple_cat(std::move(tail), std::tuple{std::move(head)})); } template auto expand(R&& receiver) && { - return detail::fold_over( - [_receiver = std::forward(receiver)]([[maybe_unused]] auto fold, auto&& first, - auto&&... rest) mutable { + return detail::fold_over([_receiver = std::forward(receiver)] + ([[maybe_unused]] Fold fold, First&& first, + Rest&&... rest) mutable { if constexpr (sizeof...(rest) == 0) { return [_receiver, - _segment = STLAB_FWD(first).append([_receiver](V&& val) { + _segment = std::forward(first).append([_receiver](V&& val) { _receiver->operator()(std::forward(val)); })](T&&... args) mutable { return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } else { - return [_receiver, _segment = STLAB_FWD(first).append( - fold(fold, STLAB_FWD(rest)...))]( + return [_receiver, _segment = std::forward(first).append( + fold(fold, std::forward(rest)...))]( T&&... args) mutable { return std::move(_segment).invoke(_receiver, std::forward(args)...); }; @@ -214,7 +219,7 @@ class chain { and see if the compiler gets it correct. */ - explicit chain(const chain&) = default; + chain(const chain&) = default; chain(chain&&) noexcept = default; chain& operator=(const chain&) = default; chain& operator=(chain&&) noexcept = default; @@ -355,7 +360,7 @@ auto start(Chain&& chain, Args&&... args) { } template -inline auto sync_wait(Chain&& chain, Args&&... args) { +auto sync_wait(Chain&& chain, Args&&... args) { using result_t = typename Chain::template result_type; struct receiver_t { @@ -381,23 +386,24 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { } bool canceled() const { return false; } - } receiver; + }; /* REVISIT: (sean-parent) - chain invoke doesn't work with std::ref(receiver). We should fix that but for now create a receiver-ref. */ - auto hold = std::forward(chain).invoke(receiver_ref{&receiver}, - std::forward(args)...); + auto receiver = std::make_shared(); - std::unique_lock lock(receiver.m); - receiver.cv.wait(lock, [&] { return receiver.result.has_value() || receiver.error; }); + std::forward(chain).invoke(receiver, std::forward(args)...); - if (receiver.error) { - std::rethrow_exception(receiver.error); + std::unique_lock lock(receiver->m); + receiver->cv.wait(lock, [&] { return receiver->result.has_value() || receiver->error; }); + + if (receiver->error) { + std::rethrow_exception(receiver->error); } - return *receiver.result; + return *receiver->result; } /* @@ -415,7 +421,6 @@ inline auto sync_wait(Chain&& chain, Args&&... args) { //-------------------------------------------------------------------------------------------------- #include -#include #include #include #include @@ -436,7 +441,7 @@ struct cancellation_source { struct cancellation_token { std::shared_ptr _state; - bool canceled() const { return _state->canceled.load(std::memory_order_relaxed); } + auto canceled() const { return _state->canceled.load(std::memory_order_relaxed); } }; // Segment that injects a cancellation_token (Injects != void) @@ -766,55 +771,20 @@ TEST_CASE("Initial draft", "[initial_draft]") { REQUIRE(46 == val.member()); } -#if 0 - auto a0 = on(default_executor) | [] { - cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; - return 42; - }; - // std::cout << typeid(decltype(a0)::result_type<>).name() << "\n"; - // auto future = start(std::move(a0)); - - auto a1 = std::move(a0) | on(default_executor) | [](int x) { - cout << "received: " << x << " on thread: " << std::this_thread::get_id() << "\n"; - // throw std::runtime_error("test-exception"); - return "forwarding: " + std::to_string(x + 1); - }; + GIVEN("a sequence of callables in a chain of chains synchronous") { + auto a0 = on(immediate_executor) | [](int x) { return x * 2; } | on(immediate_executor) | [](int x) { return to_string(x); } | + on(immediate_executor) | [](const string& s) { return s + "!"; }; - cout << "Main thread: " << std::this_thread::get_id() << "\n"; - cout << "Ready to go async!\n"; -#endif -#if 0 - auto a2 = then(std::move(a1)) | [](std::string s) { - cout << s << "<-- \n"; - return 0; - }; -#endif - -#if 0 - { - auto f = std::move(a1)(); // start and cancel. - std::this_thread::sleep_for(1ns); + auto f = start(std::move(a0), 42); + auto val = f.get_ready(); + REQUIRE(val == string("84!")); } -#endif -#if 0 - // TODO: (sean-parent) await on a chain can be optimized. + GIVEN("a sequence of callables in a chain of chains asynchronous") { + auto a0 = on(default_executor) | [](int x) { return x * 2; } | on(immediate_executor) | [](int x) { return to_string(x); } | + on(default_executor) | [](const string& s) { return s + "!"; }; - try { - std::cout << any_cast(await(std::move(a1)())) << "\n"; - } catch(const std::exception& error) { - std::cout << "exception: " << error.what() << "\n"; + auto val = sync_wait(std::move(a0), 42); + REQUIRE(val == string("84!")); } -#endif - - // std::this_thread::sleep_for(3s); - - // std::cout << await(start(std::move(a1))) << "\n"; - - // auto future = start(std::move(a1)); - // auto a2 = then(future) | [](std::string s) { return s + "<-- \n"; }; - - // std::cout << sync_wait(std::move(a2)) << "\n"; - - // pre_exit(); } diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..5e1cf68 --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,13 @@ +#define CATCH_CONFIG_RUNNER + +#include + +#include + +int main(int argc, char** argv) { + int result = Catch::Session().run(argc, argv); + + stlab::pre_exit(); + + return result; +} \ No newline at end of file From 620e246827b11eb623c6353398f367ad33f8a57b Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 21 Nov 2025 18:53:49 +0100 Subject: [PATCH 15/16] Fix include guard --- include/chains/tuple.hpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index dd7c622..6c4edd2 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -1,3 +1,6 @@ +#ifndef CHAIN_TUPLE_HPP +#define CHAIN_TUPLE_HPP + #include // std::size_t #include // std::invoke #include // std::tuple, std::get, std::apply, std::tuple_size_v @@ -5,9 +8,6 @@ #include // std::forward, std::move, std::index_sequence, std::make_index_sequence #include // std::monostate -#ifndef CHAIN_TUPLE_HPP -#define CHAIN_TUPLE_HPP - namespace chains::inline v0 { namespace detail { @@ -60,7 +60,6 @@ struct find_max_prefix { template constexpr auto invoke_prefix(F&& f, Tuple&& t) { if constexpr (K == 0) { - // No arguments: only attempt if callable with () if constexpr (requires(F&& f2) { std::invoke(f2); }) { if constexpr (std::is_void_v) { std::invoke(f); From 7b487995b51d291658cb990a6f46780814ac6b77 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 21 Nov 2025 22:54:32 +0100 Subject: [PATCH 16/16] Cleanup Start thinking about `f = start(then(fut))` problem --- include/chains/tuple.hpp | 6 +-- test/initial_draft.cpp | 87 +++++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 6c4edd2..1e4ecf6 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -127,11 +127,11 @@ constexpr auto tuple_consume(Tuple&& values) { if constexpr (consumed == 0) { // Remaining is original tuple (no elements consumed) - return std::tuple_cat(std::make_tuple(std::move(result)), std::move(_values)); + return std::tuple_cat(std::tuple{std::move(result)}, std::move(_values)); } else { auto remaining = tuple_tail_at( std::move(_values), std::make_index_sequence{}); - return std::tuple_cat(std::make_tuple(std::move(result)), std::move(remaining)); + return std::tuple_cat(std::tuple{std::move(result)}, std::move(remaining)); } }; } @@ -156,7 +156,7 @@ constexpr auto calc_step(F& f, T t) { template constexpr auto calc(F f, Args&&... args) { - return calc_step<0>(f, std::make_tuple(std::forward(args)...)); + return calc_step<0>(f, std::tuple{std::forward(args)...}); } template diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 54720f4..0d94ed6 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -77,7 +77,7 @@ class segment { The basic operations should follow those from C++ lambdas, for now default everything. and see if the compiler gets it correct. */ - segment(const segment&) = default; + explicit segment(const segment&) = default; segment(segment&&) noexcept = default; segment& operator=(const segment&) = default; segment& operator=(segment&&) noexcept = default; @@ -86,7 +86,7 @@ class segment { auto append(F&& f) && { return chains::segment{ type{}, std::move(_apply), - std::tuple_cat(std::move(_functions), std::make_tuple(std::forward(f)))}; + std::tuple_cat(std::move(_functions), std::tuple{std::forward(f)})}; } #if 0 @@ -177,14 +177,14 @@ class chain { return [_receiver, _segment = std::forward(first).append([_receiver](V&& val) { _receiver->operator()(std::forward(val)); - })](T&&... args) mutable { - return std::move(_segment).invoke(_receiver, std::forward(args)...); + })](Args&&... args) mutable { + return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } else { return [_receiver, _segment = std::forward(first).append( - fold(fold, std::forward(rest)...))]( - T&&... args) mutable { - return std::move(_segment).invoke(_receiver, std::forward(args)...); + fold(fold, std::forward(rest)...))]( + Args&&... args) mutable { + return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } }, @@ -232,7 +232,7 @@ class chain { template auto append(segment&& head) && { - return chains::chain{std::tuple_cat(std::move(_tail), std::make_tuple(std::move(_head))), + return chains::chain{std::tuple_cat(std::move(_tail), std::tuple{std::move(_head)}), std::move(head)}; } @@ -314,15 +314,15 @@ auto on(E&& executor) { `auto f = start(then(future));` And we destruct f. We need to _delete_ the (detached) future. Where is this held? f is only - holding the promise. + holding the promise. */ template auto then(F&& future) { return chain{std::tuple<>{}, segment{type::result_type>{}, - [_future = std::forward(future)](auto&& f) mutable { - return std::move(_future).then(std::forward(f)); + [_future = std::forward(future)](C&& continuation) mutable { + return std::move(_future).then(std::forward(continuation)); }}}; } @@ -330,35 +330,42 @@ auto then(F&& future) { // TODO: (sean-parent) - fix case where invoke_t is void. template -auto start(Chain&& chain, Args&&... args) { - using result_t = typename Chain::template result_type; - +inline auto start(Chain&& chain, Args&&... args) { + using result_t = Chain::template result_type; using package_task_t = stlab::packaged_task; - auto shared = std::shared_ptr(); - // Build the receiver and future first. - auto [receiver, future] = stlab::package( - stlab::immediate_executor, - [_shared = shared](T&& val) { return std::forward(val); }); + using invoke_t = decltype(std::forward(chain).invoke( + std::declval>>(), + std::forward(args)...)); + + auto shared_receiver = std::shared_ptr(); - // Promote receiver to shared_ptr to extend lifetime beyond this scope. - shared = std::make_shared(std::move(receiver)); + if constexpr (std::is_same_v) { + auto [receiver, future] = stlab::package( + stlab::immediate_executor, [](T&& val) { return std::forward(val); }); - // Recompute invoke_t based on passing the shared_ptr (pointer semantics). - using invoke_t = - decltype(std::forward(chain).invoke(shared, std::forward(args)...)); + // Promote receiver to shared_ptr to extend lifetime beyond this scope and circumvent the + // move only capabilities of package_task. + shared_receiver = std::make_shared(std::move(receiver)); - if constexpr (std::is_void_v) { - // Just invoke; lifetime of receiver is now owned by captures inside the async chain. - std::forward(chain).invoke(shared, std::forward(args)...); + std::forward(chain).invoke(std::move(shared_receiver), std::forward(args)...); + + return std::move(future); } else { - // Keep any handle the chain returns (e.g. continuation future or cancellation handle). - auto hold = std::forward(chain).invoke(shared, std::forward(args)...); - (void)hold; // store or return if you later need it + auto p = std::make_shared>(); + auto [receiver, future] = stlab::package( + stlab::immediate_executor, + [p](V&& value) { return std::forward(value); }); + + shared_receiver = std::make_shared(std::move(receiver)); + + *p = std::forward(chain).invoke(std::move(shared_receiver), + std::forward(args)...); + return std::move(future); } - return std::move(future); } + template auto sync_wait(Chain&& chain, Args&&... args) { using result_t = typename Chain::template result_type; @@ -465,7 +472,7 @@ auto on_with_cancellation(E&& executor, cancellation_source source) { cancellation_token token{_source._state}; std::move(_executor)( [_f = std::forward(f), _token = token, - _args = std::make_tuple(std::forward(args)...)]() mutable noexcept { + _args = std::tuple{std::forward(args)...}]() mutable noexcept { std::apply( [&_f, &_token](As&&... as) { std::forward(_f)(_token, std::forward(as)...); @@ -788,3 +795,19 @@ TEST_CASE("Initial draft", "[initial_draft]") { REQUIRE(val == string("84!")); } } + + +TEST_CASE("Cancellation of then()", "[initial_draft]") { + annotate_counters cnt; + GIVEN("that a ") { + auto fut = async(default_executor, [] { + std::this_thread::sleep_for(std::chrono::seconds{3}); + std::cout << "Future did run" << std::endl; + return std::string("42"); + }).then([_counter = annotate{cnt}](const auto& s) { std::cout << s << std::endl; }); + + auto result_f = start(then(fut)); + } + std::this_thread::sleep_for(std::chrono::seconds{5}); + std::cout << cnt << std::endl; +} \ No newline at end of file