result_t foo(params_t);
...
auto result = foo(params);
- make it async by adding a continuation: void async_foo(params_t params, invokable<result_t> cont) { cont(foo(params)); };
...
invokable<result_t> auot receiver= [](result_t result) {...};
async_foo(params, receiver);
- then curry it: auto curried_async_foo(params_t params) {
return [params](invokable<result_t> cont) {
async_foo(params, cont);
};}
...
auto op = curried_async_foo(params);
op(receiver);
- finally modify the curried variant to add another required evaluation round: auto foo_sender(param_t params) {
return [params](invokable<result_t> cont) {
return [params, cont]{
async_foo(params, cont);
};};}
...
auto sender = foo_sender(params);
auto operation = sender(receiver);
operation();
The actual library uses structures with named methods instead of callables (so you would do operation.start() for example), plus a separate continuation for the error path (by having the receiver concept implement two named functions), and cancellation support. Also the final operation is required to be address stable until it is completed.The complexity is of course in the details, and I didn't fully appreciate it until I tried to implement a stripped down version of the model (and I'm sure I'm still missing a lot).
The model does work very well with coroutines and can avoid or defer a lot of the expected memory allocations of async operations.
Is this true in realistic use cases or only in minimal demos? From what I've seen, as soon as your code is complex enough that you need two compilation units, you need some higher level async abstraction, like coroutines.
And as soon as you have coroutines, you need to type-erase both the senders and the scheduler, so you have at least couple of allocations per continuation.
I did manage to co_await senders without additional allocations though (but the code I wrote is write-only so I need to re-understand what I did 6 months ago again).
I recall that I was able to allocate the operation_state as a coroutine-local, and the scheduler is node based, so the operation_state can just be appended to the scheduler queue.
You still do need to allocate the coroutine itself, and I haven't played with coroutine allocators yet.
And it does look like an interesting proposal
My reduction to continuations is, I think, derived form Eric Niebler original presentation where he introduced a prototype of the idea behind sender/receivers.
The authors and NVIDIA do not guarantee that this code is fit for any purpose whatsoever.
And say that it worries them. This is actually a warranty disclaimer (the warranty of fitness for a particular purpose) and has to be written like this to be effective. So I would not read anything into it
Even with different formatters I'd much prefer the tbb variant.
"Hmm, we need a new keyword to express this complicated idea about how geese move, in other languages they seem to use the keyword fly"
A1: Just re-use this existing four letter keyword 'swim'. Yeah, technically a goose can also swim but that's different, and the ideas aren't similar, and the implementation is entirely different, but this way we didn't need a new keyword so it's less work for a compiler right?
A2: Simple, new keyword complicated_goose_motion - why are people not happy with that?
At some point a noob will ask "Hey why can't we just name it `fly` like in Other Language?" and they will be ridiculed because of course several C++ companies have used the word fly as an identifier in software, and so reserving that word would incur a slight annoyance for them, whereas just forcing either A1 or A2 avoids a little work for those companies and is thus better...
I’d like to see an example of a task that can be done with less verbosity in C++ than say, Python, using only the standard library
A lot of it is about making metaprogramming a lot easier to write and to read.
No more enable_if kludges since if constexpr (and concepts for specific stuff); and using concepts allows to better communicate intent (e.g.: template<typename I> can become template<std::integral I> if you want the template to be used only with integer types, and so on)