2
\$\begingroup\$

Second try.

This time rather than get() and operator bool make the handle be a range operator.

The only difference in the promise is: std::suspend_never initial_suspend() noexcept {return {};}. Here return std::suspend_never instead of std::std::suspend_always forcing the co-routine to run to the first yield point before the owner takes control back.

  #include <iostream>
  #include <coroutine>

  struct Handle
  {
      struct Promise;
      using CoRoutineHandle = std::coroutine_handle<Promise>;

      struct Promise
      {
          // Place to store the output value from yield and return.
          int   output;

          // This function is used to build the return object from the function.
          Handle get_return_object()                      noexcept    {return Handle{CoRoutineHandle::from_promise(*this)};}

          // Returning std::suspend_never by initial_suspend means we run to the first yield point (or exit the co-routine).
          
          // This means the output value will have an initial value already set
          // assuming we hit a co_yield.
          std::suspend_never initial_suspend()           noexcept    {return {};}

          // This is called by co_yield.
          // Using std::suspend_always means the control is returned to the caller.
          std::suspend_always yield_value(int value)      noexcept    {output = value;return {};}

          // Return std::suspend_always to make sure
          // the co-routine does not exit prematurely and it is easy to detect that
          // we have reached the exit with promise.done()
          std::suspend_always final_suspend()             noexcept    {return {};}

          // This is called if there was an exception.
          // Needed but not used.
          void unhandled_exception()                      noexcept    {}
      };

      // Constructor keeps track of the handle.
      CoRoutineHandle  handle;
      Handle(CoRoutineHandle handle)
          : handle(handle)
      {}

      // Need to manually destroy the handle.
      // When we have finihsed using the Promise
      ~Handle()
      {
          handle.destroy();
      }

      // Simplest iterator I could write.
      // Iterator are only every equal if they are both at then end.
      class Iter
      {
          CoRoutineHandle     handle;
          bool                atEnd;
          public:
              // Note: The creator of the iterator
              //       Way want this to be begin. But
              //       if the co-routine is done there are no
              //       more values then the begin is the end.
              Iter(CoRoutineHandle handle, bool isEnd = false)
                  : handle(handle)
                  , atEnd(isEnd || handle.done())
              {}
              // I just find it more intuative to read comparisons like this.
              // rather than make it a member the friend test just looks simpler.
              friend bool operator==(Iter const& lhs, Iter const& rhs)
              {
                  return lhs.atEnd && rhs.atEnd;
              }
              // ++ moves to the next value.
              // If this is the `final_suspend` point then this is now
              // the end value and so not valid.
              void operator++()
              {
                  handle.resume();
                  atEnd = handle.done();
              }
              // Handle always contains a valid value.
              // as we let the co-routine run to the first yield.
              // Note: if there are no yield poitns hit then
              // the iterator has already been marked as "atEnd"
              // Thus calling this is UB.
              int  operator*()
              {
                  return handle.promise().output;
              }
      };

      Iter begin()    {return {handle};};
      Iter end()      {return {handle, true};}
  };

  namespace std
  {
      template<typename... Args>
      struct coroutine_traits<Handle, Args...>
      {
          using promise_type = Handle::Promise;
      };
  }

  Handle myLazyRange(int b, int e)
  {
      while (b < e) {
          co_yield b++;
      }
  }

Now it can be used like this:

  int main()
  {
      Handle range = myLazyRange(10, 15);
      for(auto val: range) {
          std::cout << val << "\n";
      }
  }
\$\endgroup\$
1
  • \$\begingroup\$ Maybe you can delete the question with the first try, as this one obsoletes it? \$\endgroup\$
    – G. Sliepen
    Commented Oct 6 at 9:15

1 Answer 1

4
\$\begingroup\$

Make it more generic

Your Handle only works with functions returning an int. It should be rather easy to make Handle a template, so it can be used with any type, making it much more useful.

It doesn't handle reuse

Consider this:

using nonLazyRange = std::vector<int>;
auto range = nonLazyRange{1, 2, 3, 4, 5};
for (auto val: range)
    std::cout << val << '\n';
for (auto val: range)
    std::cout << val << '\n';

What do you expect the output to be? What happens if you try it with myLazyRange? Do you think it should behave differently?

Your class also doesn't handle being copied:

auto range1 = myLazyRange(10, 15);
auto range2 = range1;

This causes a segmentation fault when range1 goes out of scope.

It silently eats exceptions

If you throw an exception inside myLazyRange(), it will act as a co_return instead, with no error being propagated to the caller. Make sure exceptions are propagated.

It's not a range

To ensure myLazyRange() returns an actual range, you should ensure the following line compiles:

std::ranges::range auto range = myLazyRange(10, 15);

Make sure your range and iterators satisfy the appropriate ranges concepts.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I think the heart of the copyability issue is a naming issue. Handle is not a handle. Handles are supposed to be lightweight references to “things” that can be freely copied and moved around. But in this case, Handle is the actual “thing”—the generator—that holds the actual data and the promise, and cannot be copied or moved. \$\endgroup\$
    – indi
    Commented Oct 7 at 10:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.