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";
}
}
2 Answers 2
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.
-
\$\begingroup\$ A
Handle
holds astd::coroutine_handle
. AlsoHandle
is a computer science term that means a pointer to a location that holds a pointer to a resource. \$\endgroup\$Loki Astari– Loki Astari2025年08月14日 23:08:33 +00:00Commented Aug 14 at 23:08 -
\$\begingroup\$ @LokiAstari Yes. my point is
Handle myLazyRange
vsIterator myLazyRange
.Handle
gives you no idea what resource is held, only that some resource is held. \$\endgroup\$Caleth– Caleth2025年08月15日 08:08:33 +00:00Commented Aug 15 at 8:08
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;
}
}
-
\$\begingroup\$ always
auto
is a valid style \$\endgroup\$Caleth– Caleth2025年08月14日 09:00:23 +00:00Commented 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\$indi– indi2025年08月14日 19:08:37 +00:00Commented Aug 14 at 19:08