diff --git a/include/continuable/continuable-promise-base.hpp b/include/continuable/continuable-promise-base.hpp index c344f43..7354c45 100644 --- a/include/continuable/continuable-promise-base.hpp +++ b/include/continuable/continuable-promise-base.hpp @@ -180,6 +180,26 @@ public: data_ = nullptr; } + /// Resolves the continuation with the cancellation token which is represented + /// by a default constructed exception_t. + /// + /// \throws This method never throws an exception. + /// + /// \attention This method may only be called once, + /// when the promise is valid operator bool() returns true. + /// Calling this method will invalidate the promise such that + /// subsequent calls to operator bool() will return false. + /// This behaviour is only consistent in promise_base and + /// non type erased promises may behave differently. + /// Invoking an invalid promise_base is undefined! + /// + /// \since 4.0.0 + void set_canceled() noexcept { + assert(data_); + std::move(data_)(exception_arg_t{}, exception_t{}); + data_ = nullptr; + } + /// Returns true if the continuation is valid (non empty). /// /// \throws This method never throws an exception. diff --git a/include/continuable/continuable-result.hpp b/include/continuable/continuable-result.hpp index 33ff9d4..c3c5289 100644 --- a/include/continuable/continuable-result.hpp +++ b/include/continuable/continuable-result.hpp @@ -46,16 +46,25 @@ namespace cti { /// - *no result*: If the operation didn't finish /// - *a value*: If the operation finished successfully /// - *an exception*: If the operation finished with an exception +/// or was cancelled. /// \{ -/// A class which is convertible to any \ref result and that definitly holds no +/// A class which is convertible to any \ref result and that definitely holds no /// value so the real result gets invalidated when this object is passed to it. /// /// \since 4.0.0 /// struct empty_result {}; -/// A class which is convertible to any result and that definitly holds +/// A class which is convertible to any \ref result and that definitely holds +/// a default constructed exception which signals the cancellation of the +/// asynchronous control flow. +/// +/// \since 4.0.0 +/// +struct cancellation_result {}; + +/// A class which is convertible to any result and that holds /// an exception which is then passed to the converted result object. /// /// \since 4.0.0 @@ -72,9 +81,8 @@ public: ~exceptional_result() = default; explicit exceptional_result(exception_t exception) - // NOLINTNEXTLINE(hicpp-move-const-arg, performance-move-const-arg) - : exception_(std::move(exception)) { - } + // NOLINTNEXTLINE(hicpp-move-const-arg, performance-move-const-arg) + : exception_(std::move(exception)) {} exceptional_result& operator=(exception_t exception) { // NOLINTNEXTLINE(hicpp-move-const-arg, performance-move-const-arg) @@ -110,6 +118,7 @@ public: /// - *no result*: If the operation didn't finish /// - *a value*: If the operation finished successfully /// - *an exception*: If the operation finished with an exception +/// or was cancelled. /// /// The interface of the result object is similar to the one proposed in /// the `std::expected` proposal: @@ -132,20 +141,17 @@ class result { template explicit result(detail::init_arg_t, Args&&... values) - : variant_(trait_t::wrap(std::forward(values)...)) { - } + : variant_(trait_t::wrap(std::forward(values)...)) {} explicit result(detail::init_arg_t, exception_t exception) - : variant_(std::move(exception)) { - } + : variant_(std::move(exception)) {} public: using value_t = typename trait_t::value_t; template explicit result(FirstArg&& first, Args&&... values) - : variant_(trait_t::wrap(std::forward(first), - std::forward(values)...)) { - } + : variant_(trait_t::wrap(std::forward(first), + std::forward(values)...)) {} result() = default; result(result const&) = default; @@ -154,13 +160,13 @@ public: result& operator=(result&&) = default; ~result() = default; - explicit result(exception_t exception) : variant_(std::move(exception)) { - } - result(empty_result) { - } - result(exceptional_result exceptional_result) - : variant_(std::move(exceptional_result.get_exception())) { - } + explicit result(exception_t exception) + : variant_(std::move(exception)) {} + /* implicit */ result(empty_result) {} + /* implicit */ result(exceptional_result exceptional_result) + : variant_(std::move(exceptional_result.get_exception())) {} + /* implicit */ result(cancellation_result) + : variant_(exception_t{}) {} result& operator=(empty_result) { set_empty(); @@ -183,6 +189,10 @@ public: void set_exception(exception_t exception) { variant_ = std::move(exception); } + /// Set the result into a state which holds the cancellation token + void set_canceled() { + variant_ = exception_t{}; + } /// Returns true if the state of the result is empty bool is_empty() const noexcept { @@ -192,7 +202,7 @@ public: bool is_value() const noexcept { return variant_.template is(); } - /// Returns true if the state of the result holds an exception + /// Returns true if the state of the result holds a present exception bool is_exception() const noexcept { return variant_.template is(); } @@ -313,18 +323,18 @@ namespace std { // The GCC standard library defines tuple_size as class and struct which // triggers a warning here. #if defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmismatched-tags" +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wmismatched-tags" #endif template struct tuple_size> - : std::integral_constant {}; + : std::integral_constant {}; template struct tuple_element> - : tuple_element> {}; + : tuple_element> {}; #if defined(__clang__) -#pragma GCC diagnostic pop +# pragma GCC diagnostic pop #endif } // namespace std diff --git a/include/continuable/detail/core/base.hpp b/include/continuable/detail/core/base.hpp index d19729d..57c3e80 100644 --- a/include/continuable/detail/core/base.hpp +++ b/include/continuable/detail/core/base.hpp @@ -381,7 +381,7 @@ inline auto invoker_of(identity) { identity<>{}); } -/// - empty_result -> +/// - empty_result -> inline auto invoker_of(identity) { return make_invoker( [](auto&& callback, auto&& next_callback, auto&&... args) { @@ -392,7 +392,27 @@ inline auto invoker_of(identity) { std::forward(args)...); // Don't invoke anything here since returning an empty result - // cancels the asynchronous chain effectively. + // aborts the asynchronous chain effectively. + (void)result; + CONTINUABLE_BLOCK_TRY_END + }, + identity<>{}); +} + +/// - cancellation_result -> +inline auto invoker_of(identity) { + return make_invoker( + [](auto&& callback, auto&& next_callback, auto&&... args) { + (void)next_callback; + CONTINUABLE_BLOCK_TRY_BEGIN + cancellation_result result = invoke_callback( + std::forward(callback), + std::forward(args)...); + + // Forward the cancellation to the next available exception handler + invoke_no_except(std::forward(next_callback), + exception_arg_t{}, exception_t{}); + (void)result; CONTINUABLE_BLOCK_TRY_END }, @@ -409,7 +429,7 @@ inline auto invoker_of(identity) { invoke_callback(std::forward(callback), std::forward(args)...); - // Forward the exception to the next available handler + // Forward the exception to the next available exception handler invoke_no_except(std::forward(next_callback), exception_arg_t{}, std::move(result).get_exception()); @@ -441,9 +461,13 @@ auto invoker_of(identity>) { } else if (result.is_exception()) { // Forward the exception to the next available handler - invoke_no_except( - std::forward(next_callback), - exception_arg_t{}, std::move(result).get_exception()); + invoke_no_except(std::forward( + next_callback), + exception_arg_t{}, + std::move(result).get_exception()); + } else { + // Aborts the continuation of the chain + assert(result.is_empty()); } // Otherwise the result is empty and we are cancelling our @@ -543,6 +567,10 @@ public: std::move(next_callback_)(exception_arg_t{}, std::move(exception)); } + void set_canceled() noexcept { + std::move(next_callback_)(exception_arg_t{}, exception_t{}); + } + explicit operator bool() const noexcept { return true; } @@ -699,9 +727,13 @@ struct callback_base, HandleResults, HandleErrors, Callback, std::move (*this)(std::move(args)...); } - /// Resolves the continuation with the given error variable. - void set_exception(exception_t error) noexcept { - std::move (*this)(exception_arg_t{}, std::move(error)); + /// Resolves the continuation with the given exception. + void set_exception(exception_t exception) noexcept { + std::move (*this)(exception_arg_t{}, std::move(exception)); + } + + void set_canceled() noexcept { + std::move (*this)(exception_arg_t{}, exception_t{}); } /// Returns true because this is a present continuation @@ -768,6 +800,10 @@ struct final_callback : util::non_copyable { std::move (*this)(exception_arg_t{}, std::move(exception)); } + void set_canceled() noexcept { + std::move (*this)(exception_arg_t{}, exception_t{}); + } + explicit operator bool() const noexcept { return true; } @@ -872,6 +908,8 @@ struct chained_continuation, identity, } else if (result.is_exception()) { util::invoke(std::move(proxy), exception_arg_t{}, std::move(result.get_exception())); + } else { + assert(result.is_empty()); } } else { // Invoke the continuation with a proxy callback. @@ -927,6 +965,8 @@ struct chained_continuation, identity, } else if (result.is_exception()) { util::invoke(std::move(proxy), exception_arg_t{}, std::move(result.get_exception())); + } else { + assert(result.is_empty()); } } diff --git a/include/continuable/detail/operations/split.hpp b/include/continuable/detail/operations/split.hpp index 229ed4a..635b830 100644 --- a/include/continuable/detail/operations/split.hpp +++ b/include/continuable/detail/operations/split.hpp @@ -93,6 +93,10 @@ public: std::move (*this)(exception_arg_t{}, std::move(error)); } + void set_canceled() noexcept { + std::move (*this)(exception_arg_t{}, exception_t{}); + } + explicit operator bool() const noexcept { bool is_valid = operator_bool_or::get(first_); traverse_pack( diff --git a/include/continuable/detail/other/testing.hpp b/include/continuable/detail/other/testing.hpp index cd7d39c..80a5e67 100644 --- a/include/continuable/detail/other/testing.hpp +++ b/include/continuable/detail/other/testing.hpp @@ -111,7 +111,7 @@ void assert_async_never_completed(C&& continuable) { FAIL(); }) - .fail([](cti::exception_t error) { + .fail([](cti::exception_t) { // ... FAIL(); }); diff --git a/test/unit-test/multi/test-continuable-base-destruct.cpp b/test/unit-test/multi/test-continuable-base-destruct.cpp index aa4c8b5..18730c6 100644 --- a/test/unit-test/multi/test-continuable-base-destruct.cpp +++ b/test/unit-test/multi/test-continuable-base-destruct.cpp @@ -104,6 +104,12 @@ TYPED_TEST(single_dimension_tests, are_not_finished_when_not_continued) { auto chain = create_incomplete(this); ASSERT_ASYNC_INCOMPLETION(std::move(chain).then(this->supply())); } + + { + ASSERT_ASYNC_INCOMPLETION(this->supply().then([] { + return empty_result(); + })); + } } TYPED_TEST(single_dimension_tests, are_not_finished_when_cancelling) { @@ -118,6 +124,46 @@ TYPED_TEST(single_dimension_tests, are_not_finished_when_cancelling) { } } +TYPED_TEST(single_dimension_tests, are_not_finished_when_cancelling_hook) { + { + ASSERT_ASYNC_CANCELLATION( + this->make(identity<>{}, identity{}, [](auto&& callback) mutable { + std::forward(callback).set_canceled(); + })); + } + + { + ASSERT_ASYNC_CANCELLATION( + this->make(identity<>{}, identity{}, [](auto&& callback) mutable { + std::forward(callback).set_exception({}); + })); + } + + { + ASSERT_ASYNC_CANCELLATION(this->supply().then([] { + return exceptional_result(exception_t{}); + })); + } + + { + ASSERT_ASYNC_CANCELLATION(this->supply().then([]() -> result<> { + return exceptional_result(exception_t{}); + })); + } + + { + ASSERT_ASYNC_CANCELLATION(this->supply().then([] { + return cancellation_result(); + })); + } + + { + ASSERT_ASYNC_CANCELLATION(this->supply().then([]() -> result<> { + return cancellation_result(); + })); + } +} + TYPED_TEST(single_dimension_tests, freeze_is_kept_across_the_chain) { { auto chain = this->supply().freeze().then([=] { diff --git a/test/unit-test/single/test-continuable-promisify.cpp b/test/unit-test/single/test-continuable-promisify.cpp index 5aee3da..9c5f353 100644 --- a/test/unit-test/single/test-continuable-promisify.cpp +++ b/test/unit-test/single/test-continuable-promisify.cpp @@ -56,7 +56,7 @@ TEST(promisify_tests, promisify_with) { auto c = cti::promisify::with( [](auto&& promise, auto&& /*e*/, int const& value) { EXPECT_EQ(value, 36354); - promise.set_exception(cti::exception_t{}); + promise.set_exception(supply_test_exception()); }, [&](auto&&... args) { async_supply(std::forward(args)...);