diff --git a/docs/Doxyfile b/docs/Doxyfile index 81190100be..c06e76abfe 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -11,7 +11,8 @@ INPUT = "$(PIKA_DOCS_DOXYGEN_INPUT_ROOT)/libs/pika/async_cuda" "$(PIKA_DOCS_DOXYGEN_INPUT_ROOT)/libs/pika/async_cuda_base" \ "$(PIKA_DOCS_DOXYGEN_INPUT_ROOT)/libs/pika/init_runtime" \ "$(PIKA_DOCS_DOXYGEN_INPUT_ROOT)/libs/pika/runtime" \ - "$(PIKA_DOCS_DOXYGEN_INPUT_ROOT)/libs/pika/execution" + "$(PIKA_DOCS_DOXYGEN_INPUT_ROOT)/libs/pika/execution" \ + "$(PIKA_DOCS_DOXYGEN_INPUT_ROOT)/libs/pika/execution_base" FILE_PATTERNS = *.cpp *.hpp *.cu RECURSIVE = YES EXCLUDE_PATTERNS = */test */detail @@ -20,4 +21,9 @@ EXTRACT_ALL = YES ENABLE_PREPPROCESSING = YES MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = YES -PREDEFINED = PIKA_EXPORT= PIKA_NVCC_PRAGMA_HD_WARNING_DISABLE= "PIKA_STATIC_CALL_OPERATOR(...)=operator()(__VA_ARGS__) const" PIKA_FORCEINLINE= +PREDEFINED = PIKA_EXPORT= \ + PIKA_FORCEINLINE= \ + PIKA_HAVE_CXX20_TRIVIAL_VIRTUAL_DESTRUCTOR= \ + PIKA_NVCC_PRAGMA_HD_WARNING_DISABLE= \ + "PIKA_STATIC_CALL_OPERATOR(...)=operator()(__VA_ARGS__) const" \ + PIKA_STDEXEC_SENDER_CONCEPT= diff --git a/docs/api.rst b/docs/api.rst index bfbffcd5a0..e507b59ab6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -138,6 +138,15 @@ All sender adaptors are `customization point objects (CPOs) :language: c++ :start-at: #include +.. doxygenclass:: pika::execution::experimental::unique_any_sender +.. doxygenclass:: pika::execution::experimental::any_sender +.. doxygenfunction:: pika::execution::experimental::make_unique_any_sender +.. doxygenfunction:: pika::execution::experimental::make_any_sender + +.. literalinclude:: ../examples/documentation/any_sender_documentation.cpp + :language: c++ + :start-at: #include + .. _header_pika_cuda: CUDA/HIP support (``pika/cuda.hpp``) diff --git a/examples/documentation/CMakeLists.txt b/examples/documentation/CMakeLists.txt index 1f443339c0..9ebff0e3bd 100644 --- a/examples/documentation/CMakeLists.txt +++ b/examples/documentation/CMakeLists.txt @@ -5,6 +5,7 @@ # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) set(example_programs + any_sender_documentation drop_operation_state_documentation drop_value_documentation hello_world_documentation diff --git a/examples/documentation/any_sender_documentation.cpp b/examples/documentation/any_sender_documentation.cpp new file mode 100644 index 0000000000..6b6675883e --- /dev/null +++ b/examples/documentation/any_sender_documentation.cpp @@ -0,0 +1,77 @@ +// Copyright (c) 2024 ETH Zurich +// +// SPDX-License-Identifier: BSL-1.0 +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include + +#include + +#include +#include +#include +#include +#include + +void print_answer(std::string_view message, + pika::execution::experimental::unique_any_sender&& sender) +{ + auto const answer = + pika::this_thread::experimental::sync_wait(std::move(sender)); + fmt::print("{}: {}\n", message, answer); +} + +int main(int argc, char* argv[]) +{ + namespace ex = pika::execution::experimental; + namespace tt = pika::this_thread::experimental; + + pika::start(argc, argv); + ex::thread_pool_scheduler sched{}; + + ex::unique_any_sender sender; + + // Whether the sender is a simple just-sender... + sender = ex::just(42); + print_answer("Quick answer", std::move(sender)); + + // ... or a more complicated sender, we can put them both into the same + // unique_any_sender as long as they send the same types. + sender = ex::schedule(sched) | ex::then([]() { + std::this_thread::sleep_for(std::chrono::seconds(3)); + return 42; + }); + print_answer("Slow answer", std::move(sender)); + + // If we try to use the sender again it will throw an exception + try + { + // NOLINTNEXTLINE(bugprone-use-after-move) + tt::sync_wait(std::move(sender)); + } + catch (std::exception const& e) + { + fmt::print("Caught exception: {}\n", e.what()); + } + + // We can also use a type-erased sender to chain work. The type of the + // sender remains the same each iteration thanks to the type-erasure, but + // the work it represents grows. + // + // However, note that using a specialized algorithm like repeat_n from + // stdexec may be more efficient. + ex::unique_any_sender chain{ex::just(0)}; + for (std::size_t i = 0; i < 42; ++i) + { + chain = std::move(chain) | ex::continues_on(sched) | + ex::then([](int x) { return x + 1; }); + } + print_answer("Final answer", std::move(chain)); + + pika::finalize(); + pika::stop(); + + return 0; +} diff --git a/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp b/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp index a3919d1754..08032dc16c 100644 --- a/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp +++ b/libs/pika/execution_base/include/pika/execution_base/any_sender.hpp @@ -710,6 +710,23 @@ namespace pika::execution::experimental { template class any_sender; + /// \brief Type-erased move-only sender. + /// + /// This class wraps senders that send types \p Ts in the value channel. This wrapper class does + /// not support arbitrary completion signatures, but requires a single value and error + /// completion signature. The value completion signature must send types \p Ts. The error + /// completion must send a \p std::exception_ptr. The wrapped sender may have a stopped + /// completion signature. + /// + /// The \ref unique_any_sender requires senders that are move-constructible and connectable with + /// r-value references to the sender. The \ref unique_any_sender itself must also be connected + /// with an r-value reference (i.e. moved when passing into sender adaptors or consumers). + /// + /// Sending references in the completion signature is not supported. + /// + /// An empty \ref unique_any_sender throws when connected to a receiver. + /// + /// \tparam Ts types sent in the value channel. template class unique_any_sender #if !defined(PIKA_HAVE_CXX20_TRIVIAL_VIRTUAL_DESTRUCTOR) @@ -727,8 +744,11 @@ namespace pika::execution::experimental { public: PIKA_STDEXEC_SENDER_CONCEPT + + /// \brief Default-construct an empty \ref unique_any_sender. unique_any_sender() = default; + /// \brief Construct a \ref unique_any_sender containing \p sender. template , unique_any_sender>>> unique_any_sender(Sender&& sender) @@ -736,6 +756,7 @@ namespace pika::execution::experimental { storage.template store>(std::forward(sender)); } + /// \brief Assign \p sender to the \ref unique_any_sender. template , unique_any_sender>>> unique_any_sender& operator=(Sender&& sender) @@ -750,6 +771,7 @@ namespace pika::execution::experimental { unique_any_sender& operator=(unique_any_sender&&) = default; unique_any_sender& operator=(unique_any_sender const&) = delete; + /// \brief Construct a \ref unique_any_sender from an \ref any_sender. // cppcheck-suppress noExplicitConstructor unique_any_sender(any_sender&& other) : storage(std::move(other.storage)) @@ -757,6 +779,7 @@ namespace pika::execution::experimental { other.reset(); } + /// \brief Assign a \ref any_sender to a \ref unique_any_sender. unique_any_sender& operator=(any_sender&& other) { storage = std::move(other.storage); @@ -800,6 +823,7 @@ namespace pika::execution::experimental { PIKA_UNREACHABLE; } + /// \brief Assign \p sender to the \ref unique_any_sender. template void reset(Sender&& sender) { @@ -810,13 +834,31 @@ namespace pika::execution::experimental { else { storage.template store>(std::forward(sender)); } } + /// \brief Empty the \ref unique_any_sender. void reset() { storage.reset(); } + /// \brief Check if the \ref unique_any_sender is empty. + /// + /// \return True if the \ref unique_any_sender is empty, i.e. default-constructed or + /// moved-from. bool empty() const noexcept { return storage.empty(); } + /// \brief Check if the \ref unique_any_sender is non-empty. + /// + /// See \ref empty(). explicit operator bool() const noexcept { return !empty(); } }; + /// \brief Type-erased copyable sender. + /// + /// See \ref unique_any_sender for an overview. Compared to \ref unique_any_sender, the \ref + /// any_sender requires the wrapped senders to be l-value reference connectable and copyable. + /// The \ref any_sender itself is also l-value reference connectable and copyable. Otherwise it + /// behaves the same as \ref unique_any_sender. + /// + /// A \ref unique_any_sender can be constructed from a \ref any_sender, but not vice-versa. + /// + /// \tparam Ts types sent in the value channel. template class any_sender #if !defined(PIKA_HAVE_CXX20_TRIVIAL_VIRTUAL_DESTRUCTOR) @@ -836,8 +878,11 @@ namespace pika::execution::experimental { public: PIKA_STDEXEC_SENDER_CONCEPT + + /// \brief Default-construct an empty \ref any_sender. any_sender() = default; + /// \brief Construct a \ref any_sender containing \p sender. template , any_sender>>> any_sender(Sender&& sender) @@ -849,6 +894,7 @@ namespace pika::execution::experimental { storage.template store>(std::forward(sender)); } + /// \brief Assign \p sender to the \ref any_sender. template , any_sender>>> any_sender& operator=(Sender&& sender) @@ -899,6 +945,7 @@ namespace pika::execution::experimental { return {std::move(moved_storage.get()), std::forward(receiver)}; } + /// \brief Assign \p sender to the \ref any_sender. template void reset(Sender&& sender) { @@ -916,10 +963,18 @@ namespace pika::execution::experimental { } } + /// \brief Empty the \ref any_sender. void reset() { storage.reset(); } + /// \brief Check if the \ref any_sender is empty. + /// + /// \return True if the \ref any_sender is empty, i.e. default-constructed or + /// moved-from. bool empty() const noexcept { return storage.empty(); } + /// \brief Check if the \ref any_sender is non-empty. + /// + /// See \ref empty(). explicit operator bool() const noexcept { return !empty(); } }; @@ -948,12 +1003,20 @@ namespace pika::execution::experimental { } } // namespace detail + /// \brief Helper function to construct a \ref unique_any_sender. + /// + /// The template parameters for \ref unique_any_sender are inferred from the value types sent by + /// the given sender \p sender. template >> auto make_unique_any_sender(Sender&& sender) { return detail::make_any_sender_impl(std::forward(sender)); } + /// \brief Helper function to construct a \ref any_sender. + /// + /// The template parameters for \ref any_sender are inferred from the value types + /// sent by the given sender \p sender. template >> auto make_any_sender(Sender&& sender) {