1

I took the implementation of coroutines for std::future from the documentation at https://en.cppreference.com/w/cpp/coroutine/coroutine_traits and modified it for std::optional

#include <coroutine>
#include <optional>
#include <iostream>

 
template <typename T, typename... Args>
requires(!std::is_void_v<T> && !std::is_reference_v<T>)
struct std::coroutine_traits<std::optional<T>, Args...> {
  struct promise_type : std::optional<T> {
 
    std::suspend_never initial_suspend() const noexcept { return {}; }
    std::suspend_never final_suspend() const noexcept { return {}; }

    std::optional<T> get_return_object() noexcept {
      return *this;
    }
 
    void return_value(const T &value)
    noexcept(std::is_nothrow_copy_constructible_v<T>) {
        **this = value;
    }
    void return_value(T &&value)
    noexcept(std::is_nothrow_move_constructible_v<T>) {
      **this = std::move(value);
    }
    void unhandled_exception() noexcept {
    }
  };
};

template <typename T>
auto operator co_await(std::optional<T> optional) noexcept
requires(!std::is_reference_v<T>) {
  struct awaiter : std::optional<T> {
    bool await_ready() const noexcept {
        return static_cast<bool>(*this);
    }
    void await_suspend(std::coroutine_handle<> cont) const {
        cont.destroy();
    }
    T await_resume() { return **this; }
  };
  return awaiter{std::move(optional)};
}



void process(std::string msg, std::optional<int> x)
{
    if(x)
        std::cout << msg  << " has value " << * x << std::endl;
    else
        std::cout << msg  << " no value" << std::endl; 
}

std::optional<int> comput(std::optional<int> a, std::optional<int> b)
{
    int c = (co_await a) * (co_await b);
    process("inner", c);
    co_return c;
}

std::optional<int> gen(){
    co_return 15;
}

int main(){

    process("final 0", gen());
    process("final 1", comput(10, 11));
    process("final 2", comput(std::nullopt, 11));
    process("final 3", comput(10, std::nullopt));
    process("final 4", comput(10, 33));

}

The results look like

final 0 no value
inner has value 110
final 1 no value
final 2 no value
final 3 no value
inner has value 330
final 4 no value

I would have expected the results to look like

final 15 no value
inner has value 110
final 1 has value 110
final 2 no value
final 3 no value
inner has value 330
final 4 has value 330

It seems to be almost working because the output of the temporary generated by the lines is correct.

int c = (co_await a) * (co_await b);
process("inner", c);

Godbolt link https://godbolt.org/z/Woah8K5rE

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217
  • Did you miss the `as_coroutine` part of the example? – Nicol Bolas Feb 27 '23 at 19:33
  • I didn't quite see the point of that and it all compiled without it. But just to be sure I created a version with it included and the same issue occurs. https://godbolt.org/z/fGfYc9vxa – bradgonesurfing Feb 27 '23 at 19:39
  • 3
    You cannot specialize any standard library template unless you are doing so with respect to at least one user-defined type. Attempting to do so yields UB. That's not what's wrong with your code, but it is still incorrect. – Nicol Bolas Feb 27 '23 at 19:40

1 Answers1

2

There is a very significant difference between your implementation and what you're linking to.

In the cppreference example, note that:

  • coroutine_traits is being specialized for std::future<T>, which get_return_object() returns.
  • the promise_type inherits from std::promise<T>.

Those are different types - promise<T> and future<T>. Which, internally, are linked:

std::future<T> get_return_object() noexcept {
    return this->get_future();
}

The future<T> returned from this->get_future() has shared state with this, so that when we do this->set_value(value), the future sees that.


In your implementation, you're specializing coroutine_traits on std::optional<T> and then (I'm simplifying slightly):

std::optional<T> get_return_object() noexcept { return *this; }
 
void return_value(const T &value) { **this = value; }

This is wrong for two reasons:

  1. First, the return_object here is just a completely distinct object of type std::optional<T> that your promise_type never has access to again, so you can never mutate it. Since the return object is just some distinct, disengaged optional that you don't have access to again, you just always get a disengaged optional back.
  2. **this = value; is simply wrong - if the current optional<T> is disengaged (which it is), this is dereferencing a disengaged optional.

In order to get this to work, your promise_type needs to have a way to mutate the returned std::optional<T>. And then, after that, you need to set the value properly.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • I think the conclusion needs some clarifying. For this code to work, the object from `get_return_object` *can't* be a `std::optional`, since as you say the promise object won't be able to mutate it. You need to return something that *isn't* a `std::optional` from `get_return_object` (which may implicitly convert to `std::optional`), which holds a reference to the real data managed by the `promise_type`. – HTNW Feb 27 '23 at 19:56
  • The explanation seems reasonable so I tried it out by creating a proxy. https://godbolt.org/z/vY7ecKP8b but the result doesn't change. Though now I'm not really sure about the lifetimes. The pointer might be dangling. – bradgonesurfing Feb 27 '23 at 20:07
  • 1
    This finally works but I had to use a shared_ptr to hold the optional which seems like a huge overhead. https://godbolt.org/z/dq8W7E6rq – bradgonesurfing Feb 27 '23 at 20:12
  • @Barry That looks super scary. You take the address of a member of a stack allocated object and then return that object and hope that the compiler elides the copy and that the user makes no other copies. Is this really safe? – bradgonesurfing Feb 27 '23 at 20:33
  • 1
    @Barry NVRO is not mandatory. Your code is not safe! – HTNW Feb 27 '23 at 20:36
  • @HTNW It's safe per Itanium ABI. I mean, you could do it [this way instead](https://godbolt.org/z/8GnrhrxTv), but either way you need to ensure that `shared_optional` isn't trivial. – Barry Feb 27 '23 at 21:04
  • @Barry It seems to work under gcc 12.2 https://godbolt.org/z/fsb6bbGfv but it doesn't work under clang 15.0 https://godbolt.org/z/8Ev1f4Yhn – bradgonesurfing Feb 27 '23 at 21:23
  • I suspect that you are correct that the object is created on the stack of the caller and asking for the address gets you what you think you ask for. *However* I suspect there is no guarantee within the coroutine ABI that there are no intermediate copies of the object returned by *return_object* before it is returned to the user. If intermediate copies are made then the pointer doesn't point to the correct object anymore. – bradgonesurfing Feb 27 '23 at 21:30
  • 2
    The NRVO version is not required to work, but the RVO/guaranteed elision version is safe. The Clang/GCC difference is https://cplusplus.github.io/CWG/issues/2563.html. – T.C. Feb 28 '23 at 03:34
  • @T.C. Thanks for that. The linked issue even explicitly mentions the clang bug is an impediment to implementing coroutines for std::optional https://github.com/llvm/llvm-project/issues/56532#issue-1305293263 I guess I will wait a little longer before trying out such things in production code. – bradgonesurfing Feb 28 '23 at 07:19