short

Structured concurrency: what std::future never had

· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

The previous short built one sender and ran it. That alone is not a reason to leave std::future behind. This is: senders compose, and the composition stays structured. You can express “run these two things concurrently, then continue once both are done” as a single value, with no thread handle to join and no lock to forget.

The program

#include <stdexec/execution.hpp>
#include <exec/static_thread_pool.hpp>
#include <print>

int main() {
    exec::static_thread_pool pool{2};
    auto sched = pool.get_scheduler();

    // Two independent pipelines, each scheduled onto the pool.
    auto a = stdexec::on(sched, stdexec::just(10) | stdexec::then([](int x) { return x + 1; }));
    auto b = stdexec::on(sched, stdexec::just(20) | stdexec::then([](int x) { return x + 2; }));

    // Run both concurrently; complete with both results once both finish.
    auto [x, y] = stdexec::sync_wait(stdexec::when_all(a, b)).value();

    std::println("a={} b={} sum={}", x, y, x + y);
}

Output:

a=11 b=22 sum=33

What just happened

A scheduler is a handle to where work runs. static_thread_pool{2} is two worker threads; pool.get_scheduler() is the handle. on(sched, snd) takes a sender and returns one that runs on that pool instead of inline.

when_all(a, b) is itself a sender. When driven, it starts a and b concurrently and completes only once both have finished, carrying both results. sync_wait blocks the calling thread until the whole graph is done and unpacks the pair.

Two things you did not have to write: a single std::mutex, and a single .join(). The concurrency is expressed in the shape of the data flow, and the pool’s destructor joins its threads when the scope ends. There is no detached thread to outlive main, no future to forget to wait on.

This is the structural win over std::future/std::promise. when_all(a, b) is a value, so you can hand it to another adaptor and keep building: schedule it, add a then that runs once both complete, race it against a timeout. A future is a leaf you can only consume; a sender is a node you can keep growing a graph from.

Compiler note

As in the previous short, this uses NVIDIA’s stdexec reference implementation, because std::execution’s headers have not landed in libstdc++/libc++ yet (June 2026). The exec:: namespace holds the implementation’s concrete schedulers (like static_thread_pool); the stdexec:: algorithms are the standard std::execution ones.

Where to next

If you have not seen the one-sender basics, start with Hello, sender. From here the model scales the same way all the way up: swap the thread pool for a GPU stream scheduler and the exact same when_all graph runs on the device.