Prompt and Scope
You must traverse a potentially huge tree or file stream and produce a value only when the consumer asks for the next one. You cannot materialize all results in memory. Use C++23 std::generator as a lazy range, explain co_yield, exceptions, and lifetime, and compare alternatives.
This coding question focuses on coroutine handles, input-range semantics, and resource boundaries. Assume one thread, one forward traversal, and that referenced external objects outlive the traversal.
What the Interviewer Evaluates
- Whether you distinguish lazy generation, materialized containers, and ordinary
views. - Whether you understand what happens at first iteration, each
++, completion, and destruction. - Whether you catch lifetime risks for locals, temporaries, references, and asynchronous resources.
- Whether you explain exception propagation, early stop, and recursive
elements_ofcosts. - Whether data size, first-item latency, peak memory, and reuse needs drive the choice.
Clarifying Questions Before Answering
- Is the consumer single-pass or does it need reuse? Single-pass favors a generator; reuse may favor a container.
- Are elements values, references, or views? References avoid copies but extend lifetime requirements.
- Does generation block on I/O, wait for events, or cross threads?
std::generatoris synchronous and does not schedule async work. - Are random access,
size, or parallel algorithms required? An input range usually does not provide them. - What happens when traversal stops early? File handles, locks, and buffers need an explicit owner and cleanup path.
30-Second Answer Framework
I model the generator as a synchronous, forward input range. co_yield suspends at each element; incrementing the consumer resumes the coroutine until the next yield, return, or exception. It fits large results that are consumed once and should produce the first item quickly. For random access, repeated traversal, or cross-thread async I/O, I choose a vector, a view pipeline, or an async stream, after checking source and resource lifetimes.
Step-by-Step Deep Dive
1. Establish the range and ownership
std::generator<T> is a C++23 synchronous coroutine range. Calling the generator function generally creates coroutine state; execution starts during iteration. The generator owns its frame, which is released when iteration ends or the generator is destroyed. Never return a reference to a local container; the caller or an outer owner must keep a referenced object alive.
2. Keep a constant working set with co_yield
#include <generator>
std::generator<int> range(int first, int last) {
for (int value = first; value < last; ++value) {
co_yield value;
}
}
void consume() {
for (int value : range(0, 1'000'000)) {
if (value == 10) break;
}
}The code does not build one million elements first; each resume advances to the next co_yield. break destroys the iterator and generator, so the coroutine frame cannot be reused. Validate C++23 support against the selected standard-library and compiler version.
3. Compare four implementations
Returning a vector is simplest and supports size, random access, and reuse, but materializes everything. A callback gives control to the producer but composes poorly with range adaptors. A hand-written input iterator works before C++23 but must maintain state, end, and exception rules. std::views suits stateless transformations over an existing range; a generator suits state machines, recursive traversal, or logic that should advance only when pulled.
4. Handle recursion and references
A tree walk can compose child generators with elements_of instead of nested loops, but measure depth, coroutine-frame count, and exception paths. If yielding std::string_view or node references, source strings and nodes must remain valid for the whole traversal. Never yield a view into a temporary string or store the generator beyond the source owner’s lifetime.
5. Handle exceptions, early stop, and resources
An exception in the generator reaches the consumer when the iterator resumes; the consumer decides whether to log, retry, or stop. break is not a business-level commit. File handles, locks, and temporary buffers should be RAII objects in the generator frame and released on destruction. This is synchronous; co_yield does not wait for a network or turn blocking I/O into async work.
6. Close with boundary benchmarks
Test empty and one-element ranges, huge ranges, recursion depth, exception aborts, and invalidated references. Compare vector, generator, and view pipelines on first-item latency, full runtime, peak RSS, allocations, repeatability, and cleanup after cancellation. Introduce coroutine complexity only when one-pass consumption and memory constraints make the lazy benefit material.
High-Quality Sample Answer
I choose std::generator when the result is large, consumed once in order, and production can pause after each item. Calling the generator creates coroutine state; the iterator resumes it until the next co_yield, so the consumer receives the first value early without materializing the entire result.
I make ownership explicit: the source tree, file, and strings outlive the generator, while coroutine handles and temporary resources use RAII. I return a vector for random access, size, or repeated traversal; use views for pure transformations over existing ranges; and use an async-stream abstraction for network waits or cross-thread work. I benchmark empty input, early break, exceptions, deep recursion, and huge input, comparing first-item latency, peak memory, throughput, and cleanup before accepting the semantic cost.
Common Mistakes
Treating a generator as an async stream
It is synchronous and cannot await a network. Use an async runtime and explicit async-stream interface instead.
Returning references or views to locals
The local can die while the coroutine is suspended. Let an owner cover the entire traversal or yield values.
Assuming break completes business cleanup
Early stop ends iteration, not an external transaction. Use RAII cleanup and explicit cancellation semantics.
Measuring only full runtime
Lazy ranges may win on first-item latency and peak memory. Measure first item, RSS, allocations, and repeatability too.
Follow-Ups and Responses
Follow-up 1: Can a generator be consumed in parallel?
One input generator is normally a single-direction state machine and should not be incremented by multiple threads. Partition the input or create independent generators and define merge order.
Follow-up 2: How do you traverse a very deep tree?
Compose child generators with elements_of but measure frames and depth. For unbounded depth, an explicit stack makes memory limits and cancellation more visible.
Follow-up 3: What if a consumer stores an element reference?
Document validity through the next increment or generator destruction unless an external owner keeps the source alive. Copy the value or transfer ownership for long-term storage.
Follow-up 4: What if a C++20 project lacks std::generator?
Use a project generator or input-iterator wrapper, or return a view, but specify ownership, end, and exception contracts. Renaming C++23 syntax does not recreate its semantics.