Representative interview topic

C++26 interview: How do senders, receivers, and operation states work together?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

Explain the roles of sender, receiver, and operation state in C++26 std::execution. What do connect and start do, and how do value, error, and stopped completions work?

Problem and context

This question tests the core object model in C++26 std::execution. Start with one minimal asynchronous operation and explain a sender's lazy description, a receiver's completion contract, the operation state produced by connect, and the point at which start launches computation.

What the interviewer evaluates

The key distinction is between a lazy sender description and an operation state created after connection: connect builds state, while start begins work. Explain how set_value, set_error, and set_stopped move through the pipeline, whether scheduler changes affect thread affinity, and how cancellation prevents new side effects.

Clarifying questions to ask first

Work and backpressure

Ask about batch size, permitted concurrency, input ordering, and retry policy for failed writes. The answers determine whether to limit sender concurrency or bound a queue.

Scheduling and stop sources

Ask how I/O and CPU schedulers are exposed, whether stops come from a timeout or a user action, and how an already-submitted system call is completed. A stop request is not forced thread termination.

Resources and commit semantics

Clarify ownership of file handles, buffers, and temporary files, whether writes are idempotent, and whether partial batches can roll back. This defines cleanup after set_error.

30-second answer framework

“I describe read, parse, and write with sender adaptors, then use scheduler adaptors to move execution contexts. The pipeline stays lazy: connect creates an operation state and start begins it. Each stage sends success values downstream, errors through seterror, and cancellation through setstopped; one stop token reaches every stage. The stop path checks before side effects, while owners finish and close submitted I/O and clean temporary files.”

Detailed solution steps

Step 1: Define value and error boundaries

Define input and output types for each stage and turn recoverable failures into explicit error senders. Do not let exceptions cross scheduler boundaries; let the final receiver record success, failure, or stop.

Step 2: Compose a lazy pipeline

Use let_value, then, or equivalent adaptors to connect reading, parsing, and writing. Composition builds a description without allocating threads or performing I/O; operation state owns any shared state that must outlive a callback.

Step 3: Connect and start

Call connect(sender, receiver) to obtain an operation state, keep it in a live scope, then call start. The receiver must outlive the operation state; asynchronous callbacks cannot reference destroyed stack objects.

Step 4: Move between execution contexts

After I/O completes, use a scheduler sender to move parsing to a CPU pool, then return writing to a bounded I/O pool. Record queue capacity and fairness; do not put blocking writes in an unbounded general pool.

Step 5: Propagate stop and backpressure

Pass a stop token to every interruptible stage. After a stop, do not enqueue new batches; cancel or finish an in-flight system call according to its API. When the queue is full, a throttling sender pauses upstream work to bound memory.

Step 6: Handle errors and partial side effects

Write to a temporary file or record a batch sequence before atomically committing. set_error triggers downstream cleanup and handle closure. Retries need limits and an idempotency key so they cannot duplicate writes.

Step 7: Verify concurrency and lifetime

Test multiple schedulers, stop races, parse failures, short writes, and early receiver destruction. Use a thread analyzer for races and measure queue time, throughput, stop latency, and unreleased operation states.

High-quality sample answer

The read sender emits batches, the parse sender runs on a CPU scheduler, and the write sender runs on a bounded I/O scheduler. The pipeline only describes dependencies; connect creates operation state and start launches it. Every stage handles value, error, and stopped completion and shares a stop token. Stopping blocks new batches, lets submitted I/O close safely, and uses temporary files plus batch IDs for idempotent retries. Tests cover scheduler hops, backpressure, stop races, and receiver lifetime.

Common mistakes

  • Mistake: Assuming a constructed sender is already running. → Why: A sender is lazy. → Fix: State the connect/start boundary.
  • Mistake: Handling exceptions but not stopped completion. → Why: Stop is a separate completion channel. → Fix: Implement both seterror and setstopped.
  • Mistake: Killing a thread on cancellation. → Why: It may own a file handle or partial write. → Fix: Propagate a stop token and unwind safely.
  • Mistake: Enqueuing batches without a bound. → Why: Missing backpressure can exhaust memory. → Fix: Bound concurrency, queue capacity, and retries.

Follow-up questions and answers

Follow-up 1: Why not use futures directly?

Sender/receiver makes scheduling, cancellation, and three completion channels composable and gives connection-time lifetime control. Futures usually need extra conventions for stop and error propagation.

Follow-up 2: Can the sender be destroyed after start?

The temporary sender description can go away, but the operation state, receiver, and captured resources must remain valid until completion. A task object or scope should own them.

Follow-up 3: Does set_stopped roll back every side effect?

No. It reports stopped completion; submitted I/O may not roll back. Temporary files, idempotent commits, or compensation are needed for consistency.

Follow-up 4: How do you prove writes are not duplicated?

Assign a stable batch ID, check committed IDs before writing, and let retries write only missing IDs. Inject failures, restarts, and stop races, then compare the commit log with the final file.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool