3
\$\begingroup\$

Lazy range generator.

I know everybody's first example for co-routines.
But want to get some feedback.

The code

 #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_always by initial_suspend means no code is run at creation.
 // The co-routine is set up. The next call to resume on the handle will run the code to
 // the next yield (await) and thus return that value.
 std::suspend_always 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();
 }
 // Move the co-routine to the next yield point
 // Or if no more yields to the final_suspend point.
 // Calling this after it returns true is UB
 operator bool() const
 {
 handle.resume();
 return !handle.done();
 }
 // Get the value out of the promise object.
 // Should only be called after a call to operator bool
 int get()
 {
 return handle.promise().output;
 }
 };
 // Hate having types with lower case letters.
 // So override the default type name
 namespace std
 {
 template<typename... Args>
 struct coroutine_traits<Handle, Args...>
 {
 using promise_type = Handle::Promise;
 };
 }
 // Define the co-routine.
 Handle myLazyRange(int b, int e)
 {
 while (b < e) {
 co_yield b++;
 }
 }

Then it can be used like this:

 int main()
 {
 auto range = myLazyRange();
 while (range)
 {
 std::cout << range.get() << "\n";
 }
 }
asked Oct 5, 2024 at 18:36
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

Naming

Handle doesn't describe what the type does. It's a (Java-style) iterator. It holds a handle. Consider renaming it to Iterator or somesuch.

Generalisation

Nothing in your code requires the data be int. Any regular type will fit. Consider making this a template.

Required members

You don't have to name Promise promise_type, but neither do you need to specialise std::coroutine_traits. You can instead add an alias. using promise_type = Promise;

Interoperability

This doesn't interoperate with many language or library features. I would expect a C++ range type to support for (int i : myLazyRange(1, 10)), yours does not. And if you did support that, you'd implicitly support large parts of <algorithm> etc.

answered Aug 14 at 9:16
\$\endgroup\$
2
  • \$\begingroup\$ A Handle holds a std::coroutine_handle. Also Handle is a computer science term that means a pointer to a location that holds a pointer to a resource. \$\endgroup\$ Commented Aug 14 at 23:08
  • \$\begingroup\$ @LokiAstari Yes. my point is Handle myLazyRange vs Iterator myLazyRange. Handle gives you no idea what resource is held, only that some resource is held. \$\endgroup\$ Commented Aug 15 at 8:08
3
\$\begingroup\$

About myLazyRange

In main function, you want to demonstrate (or test) myLazyRange usage. The input parameters should be given appropriately like auto range = myLazyRange(1, 10); to make the code be more concrete. Moreover, it's nice to make the parameter name be the full name like Handle myLazyRange(int begin, int end) to enhance the readability. On the other hand, it is good to add step parameter in myLazyRange to represent the incremental step.

 // Define the co-routine.
 Handle myLazyRange(int begin, int end, int step = 1)
 {
 while (begin < end) {
 co_yield begin;
 begin += step;
 }
 }
answered Aug 14 at 7:28
\$\endgroup\$
2
  • \$\begingroup\$ always auto is a valid style \$\endgroup\$ Commented Aug 14 at 9:00
  • \$\begingroup\$ Adding a step parameter adds ENORMOUS complexity that just doesn’t exist for the simple case. It usually isn’t worth it, and if you really, really need it, it would be better to have it as a separate overload, and leave the simple case simple. \$\endgroup\$ Commented Aug 14 at 19:08

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.