C++20 coroutines: implementing an awaitable future
Asked Answered
D

1

14

Since the Coroutines TS has been accepted into C++20 at the ISO meeting at Kona, I started playing around with them a bit for myself. Clang already has decent support for coroutines but the implementation of the library support is still lacking. In particular, the Awaitable types such as std::future, std::generator, etc. have not been implemented yet.

Thus, I took it upon myself to make std::future awaitable. I largely followed the talk by James McNellis at CppCon 2016, specifically this slide:

Screengrab at timestamp 37:41 of James McNellis talk at CppCon 2016

This being 2019, I actually had some trouble with the (presumably untested?) code on this slide:

  • It seems to me overloading operator co_await is not a thing anymore? Instead one should use the optional await_transform of the promise_type. Not sure I got this right, though.
  • The then continuation of the future captures the handle by value, but the resume member function is not const-qualified. I worked around this by making the lambda mutable.

Also, then and is_ready are not available in std::future but are part of std::experimental::future which is still missing from my libc++ version. To avoid dealing with the Awaiter and to implement future continuations, I wrote a derived future class which is Awaitable and an Awaiter. It is my understanding that eventually both would also be true of std::future. You can see my example on Compiler Explorer. It does compile.

However, it also does segfault. This happens in await_resume when get() is called. This is actually not surprising since valid() returns false at that point (making the call to get() UB). I think this is because when then is used to continue the future, the original future object is moved into the async future, thus invalidating the old future (*this at the time await_resume is called, so after the move). My implementation of then is loosely inspired by this answer and this code I found on GitHub. Those may not be ideal, but cppreference explicitly states valid() == false as a postcondition of calling then, so I believe it is correct to move out of the original future.

What am I missing here? This "bug" seems present already in the above slide. How can I reconcile this issue? Does anybody know of a (working) existing implementation of an Awaitable future? Thanks.

Devin answered 9/3, 2019 at 23:19 Comment(3)
"Since the Coroutines TS has been accepted into C++20 at the ISO meeting at Kona" It should be noted that the Coroutines TS is not exactly what was adopted into C++20. The C++20 coroutines feature is similar to the Coroutines TS, but not 1:1 identical.Jadotville
"Does anybody know of a (working) existing implementation of an Awaitable future?" github.com/lewissbaker/cppcoro. Note that the author also has a blog with explanations e.g. lewissbaker.github.io/2017/11/17/…Squeal
here is how std::future can be made awaitable.Septennial
F
2

As you mentioned yourself, the issue is because the future has moved-from after calling .then(). The trick is to move it back when it's ready. This can be done if the continuation passed to .then() takes the future, not the value it holds.

Here are the functions I took from your code and changed. I also changed them from passing things to std::async as parameters to just capturing them, as this looks more intuitive to me, but this isn't the important change here.

    template <typename Work>
    auto then(Work&& w) -> co_future<decltype(w())> {
        return { std::async([fut = std::move(*this), w = std::forward<Work>(w)]() mutable {
            fut.wait();
            return w();
        })};
    }

    template <typename Work>
    auto then(Work&& w) -> co_future<decltype(w(std::move(*this)))> {
        return { std::async([fut = std::move(*this), w = std::forward<Work>(w)]() mutable {
            return w(std::move(fut));
        })};
    }

    void await_suspend(std::experimental::coroutine_handle<> ch) {
        then([ch, this](auto fut) mutable {
            *this = std::move(fut);
            ch.resume();
        });
    }

BTW, VS2017 complains about having both set_exception() and unhandled_exception() in the promise type. I removed set_exception() and changed unhandled_exception() to this:

    void unhandled_exception() {
        _promise.set_exception(std::current_exception());
    }
Flavouring answered 6/4, 2019 at 20:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.