Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/intro-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<!-- SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -->

# Introduction by Example

This page provides a series of examples showing how to use the
`std::execution` components.

<details>
<summary>`"Hello, world"` - synchronous using asynchronous components</summary>

Code: [`examples/intro-1-hello-world.cpp`]()

The first example is a very complicated way to a version of `"Hello,
world"`: it uses components for dealing with asynchronous work to
synchronously produce the result. The intention is to show a basic
use of some involved components to build up a feeling of how things
work.

The componentes for `std::execution` are declared in the header
`<execution>`. This particular implementation implements the
cmponents in namespace `beman::execution26` declared in the header
`<beman/execution26/execution.hpp>`:

```c++
#include <beman/execution26/execution.hpp>
#include <iostream>
#include <string>
#include <tuple>

namespace ex = ::beman::execution26;
using namespace std::string_literals;
```

Most of these declarations should be familiar. The namespace alias
`ex` is used to support easy migration to a different implementation,
in particular the standard name `std::execution` once it becomes
available with standard library implementations. The other examples
will have a similar start which is only mentioned in the explanation
to point out unusual parts like the use of custom components.

All interesting work happens in the `main` function:

```c++
int main()
{
auto[result] = ex::sync_wait(
ex::when_all(
ex::just("hello, "s),
ex::just("world"s)
)
| ex::then([](auto s1, auto s2){ return s1 + s2; })
).value_or(std::tuple(""s));

std::cout << result << '\n';
}
```

This code code be simpler even when using components from
`std::execution`. Showing a few more components is intended to
better reflect how an asynchronous program might look like. This
examples uses a _sender factory_ (`ex::just`), two _sender adaptors_
(`ex::when_all` and `ex::then`), and finally a _sender consumer_
(`ex::sync_wait`) to build up work and to execute it. The idea of
a _sender_ is that it represents work which can be composed with
algorithms into a unit of work which is eventually executed.

Each work item can complete asynchronously at some later time, i.e.,
calling it like a function and using a returned value isn't really
an option. Instead, when the work is started it does whatever is
needed to get the work completed and get a _completion signal_
delivered. Delivering a completion signal consists of calling a
function on a suitable objects. The important part is that once
work is started it always delivers exactly one completion signal
which can indicate success, failure, or cancellation. Later examples
for creating senders will go into more details about the cancellation
signals.

The components used in this example do all of that synchronously:

- `ex::just("string"s)` completes immediately when started with
successful completion which includes the string passed as
argument.
- <code>ex::when_all(_sender1_, _sender2_)</code> starts the senders
passed as arguments. When all of the senders complete, it
produces its own completion. In the case of success all the
received values are passed to the completion signal. In case
of an error all outstanding work is cancelled and the first
error becomes `when_all`'s completion signal once all children
have completed. Similarly, in case of cancellation all children
get cancelled and once all complete `when_all` produces a
cancellation signal. In the example the two children each produces
one string as completion signal and `when_all` produces these two
strings as its completion signal.
- <code>_sender_ | ex::then(_fun_)</code> is equivalent to using
<code>ex::then(_sender_, _fun_)</code>. The `ex::then` calls
the function <code>_fun_</code> with its child sender completes
successful. The arguments to <code>_fun_</code> are the values
received from the child completion signal. In the example, the
child is `when_all(...)` and it produces two strings which are
passed to <code>_fun_</code>. The completion signal of `ex::then`
is successful with the value returned from the call to
<code>_fun_</code> (which may `void`) if the call returns
normally. If an exception is thrown `ex::then` completes with
an `std::exception_ptr` to the exception thrown. In the example
the completion is just a concatenation of the two strings.
- <code>sync_wait(_sender_)</code> starts its argument and then
blocks until the work completes although the thread calling
`sync_wait` may contribute to the completion of the work. The
function returns a an
<code>std::optional&lt;std::tuple&lt;_results_...&gt;&gt;</code>>.
If the child sender completes successfully the values from the
child's completion signal become the elements of the tuple. If
the child completes with an error, the error is thrown as an
exception. Otherwise, if the work gets cancelled, an empty
`std::optional<...>` is returned. In the example, the child
sends a string which gets wrapped into a `std::tuple` which in
turn gets wrapped into an `std::optional`. Thus, the somewhat
round-about way to get the result: first using
`value_or(std::tuple(""s))` to get the value from the `std::optional`
which is then decomposed from the `std::tuple` using structured
bindings.

</details>

<details>
<summary>`"Hello, async"` - a simple asynchronous example</summary>

Code: [`examples/intro-2-hello-async.cpp`]()

</details>
3 changes: 3 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ list(
stop_token
stopping
allocator
intro-1-hello-world
intro-2-hello-async
intro-5-consumer
doc-just
doc-just_error
doc-just_stopped
Expand Down
27 changes: 27 additions & 0 deletions examples/intro-1-hello-world.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// examples/intro-1-hello-world.cpp -*-C++-*-
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <beman/execution26/execution.hpp>
#include <iostream>
#include <string>
#include <tuple>

namespace ex = ::beman::execution26;
using namespace std::string_literals;

// ----------------------------------------------------------------------------
// Please see the explanation in docs/intro-examples.md for an explanation.

int main() {
// clang-format off
auto [result] = ex::sync_wait(
ex::when_all(
ex::just("hello, "s),
ex::just("world"s)
) | ex::then([](auto s1, auto s2) { return s1 + s2; })
).value_or(std::tuple(""s)
);
// clang-format on

std::cout << result << '\n';
}
39 changes: 39 additions & 0 deletions examples/intro-2-hello-async.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// examples/intro-2-hello-async.cpp -*-C++-*-
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <beman/execution26/execution.hpp>
#include "intro-timer.hpp"
#include <chrono>
#include <iostream>
#include <string>
#include <tuple>

namespace ex = ::beman::execution26;
using namespace std::string_literals;
using namespace std::chrono_literals;

// ----------------------------------------------------------------------------
// Please see the explanation in docs/intro-examples.md for an explanation.

int main() {
std::cout << std::unitbuf;
intro::timer timer;

// clang-format off
auto [result] = ex::sync_wait(
ex::when_all(
timer.run(),
ex::when_all(
timer.resume_after(3s)
| ex::then([] { std::cout << "h\n"; return std::string("hello"); }),
timer.resume_after(1s)
| ex::then([] { std::cout << ",\n"; return std::string(", "); }),
timer.resume_after(2s)
| ex::then([] { std::cout << "w\n"; return std::string("world"); })
) | ex::then([](auto s1, auto s2, auto s3) { return s1 + s2 + s3; })
)
).value_or(std::tuple(std::string("")));
// clang-format on

std::cout << result << "\n";
}
93 changes: 93 additions & 0 deletions examples/intro-5-consumer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// examples/intro-1-hello-world.cpp -*-C++-*-
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <beman/execution26/execution.hpp>
#include <beman/execution26/detail/suppress_push.hpp>
#include <expected>
#include <iostream>
#include <string>
#include <tuple>

namespace ex = ::beman::execution26;
using namespace std::string_literals;

enum class success { one };
enum class failure { fail_one };

struct expected_to_channel_t {
template <typename Receiver>
struct own_receiver {
using receiver_concept = ex::receiver_t;
Receiver* receiver;
template <typename Value, typename Error>
auto set_value(std::expected<Value, Error>&& exp) noexcept -> void {
if (exp) {
std::cout << "received an expected with value from child/upstream\n" << std::flush;
ex::set_value(std::move(*receiver), exp.value_or(Value{}));
std::cout << "set_value\n" << std::flush;
} else {
std::cout << "received an expected with error from child/upstream\n";
ex::set_error(std::move(*receiver), exp.error_or(Error{}));
}
}
template <typename Error>
auto set_error(Error&& error) noexcept -> void {
std::cout << "received an error from child/upstream";
ex::set_error(std::move(*receiver), std::forward<Error>(error));
}
auto set_stopped() noexcept -> void {
std::cout << "received an cancelletion from child/upstream";
ex::set_stopped(std::move(*receiver));
}
};
template <ex::sender CSender, ex::receiver Receiver>
struct state {
using operation_state_concept = ex::operation_state_t;
using child_state_t = decltype(ex::connect(std::declval<CSender>(), std::declval<own_receiver<Receiver>>()));
Receiver parent_receiver;
child_state_t child_state;
template <ex::sender S, ex::receiver R>
state(S&& child_sender, R&& parent_receiver)
: parent_receiver(std::forward<R>(parent_receiver)),
child_state(ex::connect(std::forward<S>(child_sender), own_receiver<Receiver>{&this->parent_receiver})) {
}
void start() & noexcept {
std::cout << "starting execpted_to_channel\n";
ex::start(child_state);
}
};

template <ex::sender CSender>
struct sender {
using sender_concept = ex::sender_t;
// value_types_of<CSender....> -> set_value_t(std::expected<T, E>)
// -> completion_signatures<set_value_t(T), set_error_t(E)>
// -> + error_type_of<CSender...>
// -> + sends_stopped<CSender...> -> set_stopped_t()
// -> unique
using completion_signatures = ex::completion_signatures<ex::set_value_t(success), ex::set_error_t(failure)>;
CSender child_sender;

template <ex::receiver Receiver>
state<CSender, std::remove_cvref_t<Receiver>> connect(Receiver&& receiver) && {
return {std::move(this->child_sender), std::forward<Receiver>(receiver)};
}
template <ex::receiver Receiver>
state<CSender, std::remove_cvref_t<Receiver>> connect(Receiver&& receiver) const& {
return {this->child_sender, std::forward<Receiver>(receiver)};
}
};

template <ex::sender CSender>
sender<std::remove_cvref_t<CSender>> operator()(CSender&& child_sender) const {
return {std::forward<CSender>(child_sender)};
}
auto operator()() const { return ex::detail::sender_adaptor{*this}; }
};
inline constexpr expected_to_channel_t expected_to_channel{};

int main() {
ex::sync_wait(ex::just(std::expected<success, failure>(success::one)) | expected_to_channel() |
ex::then([](success) noexcept { std::cout << "success\n"; }) |
ex::upon_error([](failure) noexcept { std::cout << "fail\n"; }));
}
Loading
Loading