std::async / Futures & Promises
<future> gives a one-shot channel for a result computed elsewhere: a std::future reads the value, a std::promise (or std::async) produces it — with exceptions propagated across the thread boundary.
Why it matters
This is the high-level “run this and give me the answer later” model: no manual mutex/condition-variable plumbing for a single result, and an exception thrown in the worker is re-thrown when you call get(). It is the cleanest way to fan out independent tasks and collect results, and it sidesteps the [[threads-std-thread|std::thread join footguns]] for compute-and-return work.
How it works
A shared state connects the two ends: the producer sets a value or exception, the consumer’s future::get() blocks until it is ready, then returns it once (get is single-use).
| Producer | Pairs with | Notes |
|---|---|---|
std::async(policy, f, args...) | returned future | runs f, captures return/exception |
std::promise<T> | p.get_future() | manual set_value / set_exception |
std::packaged_task<R(Args)> | t.get_future() | wraps a callable for a thread/pool |
std::asynctakes a launch policy:launch::asyncforces a new thread;launch::deferredruns lazily on the calling thread atget(); the defaultasync|deferredlets the implementation choose — so it may never run in parallel.future::get()blocks, moves the result out, and re-throws any stored exception.wait_for/wait_untilpoll without consuming;valid()is false afterget().- A
std::promiseis the explicit form: a worker holds it and callsset_value(x)orset_exception(...); the other side already holds the matchingfuture. - For multiple consumers of one result, convert with
future::share()into astd::shared_future.
Example
std::future<int> f = std::async(std::launch::async,
[]{ return heavy(); }); // forced onto a thread
// ... do other work meanwhile ...
int result = f.get(); // blocks; rethrows if heavy() threwManual promise hand-off across threads:
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([&]{ p.set_value(compute()); }); // producer end
int r = f.get(); // consumer end, blocks
t.join();Pitfalls
- The
std::asyncreturn value’s destructor blocks. A discardedstd::async(...)(default/async policy) future joins in its destructor — soasync(f); async(g);runs sequentially, not in parallel. Keep the futures named and alive. - Default launch policy may defer. Without
launch::async, the task can run lazily atget()on your thread; if you never callget()/wait(), it may never run at all. get()is one-shot. Calling it twice on afutureis UB; useshared_futurefor multiple reads.- Broken promise: destroying a
std::promisewithout setting it makes the waitingget()throwstd::future_error{broken_promise}— always set a value or exception on every path.