Worked examples — std - promise and std - future
Before we start, one picture of the whole thing so every later example points back to it. A promise and a future share a single heap block — the shared state.

Read the figure left to right. The magenta box on the left is the std::promise — the writing end. The violet box on the right is the std::future — the reading end. The dashed navy arrow across the top is prom.get_future(): called once, it hands the reading end its link to the shared block. The orange box at the bottom is the shared state itself, living on the heap, holding four things: a slot for the value or an exception, a ready flag, and the two synchronization tools — a mutex (call it m, a lock that lets only one thread touch the box at a time) and a condition variable (call it cv, a "waiting bench" a thread can sleep on until it is woken). The magenta arrow is set_value / set_exception writing into the box once; the violet arrow is get() reading out of it once. Every example below is a different journey along these two arrows.
Two identifiers from that picture will keep coming back, so we name them now, once:
- ==
m== — the mutex (from mutex): a lock guaranteeing only one thread reads or writes the box at any instant, so there is no data race. - ==
cv== — the condition variable (from condition_variable): the bench a thread sleeps on insideget()untilset_valuewakes it. A ==lk== is just aunique_lockonm— the handle a thread holds while it sleeps oncv, released automatically while asleep and re-acquired on waking.
The scenario matrix
Every use of promise/future lands in exactly one of these cells. Think of the columns as "what got written" and the rows as "the timing / who reads".
| Cell | Situation | What the future's get() does |
|---|---|---|
| A. Value, reader waits | Producer sets a value; consumer already blocked in get() |
Sleeps, is woken, returns the value |
| B. Value, value-set-first | Producer sets the value before consumer calls get() |
Returns instantly — no sleep |
| C. Exception transported | Producer stores an exception | get() re-throws it in the consumer thread |
| D. Broken promise | Producer is destroyed without setting anything | get() throws future_error(broken_promise) |
| E. One-shot violation | Consumer calls get() twice, or get_future() twice |
Throws future_error(no_state / future_already_retrieved) |
| F. Zero / degenerate payload | promise<void> — a pure "done" signal, no data |
get() returns nothing, just unblocks |
| G. Multiple readers | Many threads must all see the same result | Plain future fails (one-shot); use [[std - shared_future |
| H. Limiting: timeout | Consumer can't wait forever | wait_for returns a status, get() avoided until ready |
| I. Real-world word problem | Two heavy computations run in parallel, main thread sums both | Two mailboxes, two get()s |
| J. Exam twist | Chained: packaged_task feeds a future; where is the promise? |
The task is the writing end |
The nine examples below hit every cell A–J. Each one names its cell.
Example 1 — Cell A: value, reader is already waiting
Forecast: guess the printed number, and guess whether
mainslept or busy-spun.
#include <future>
#include <thread>
#include <chrono>
#include <iostream>
void worker(std::promise<int> p) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
p.set_value(6 * 7); // Step 3
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future(); // Step 1
std::thread t(worker, std::move(prom)); // Step 2
std::cout << fut.get() << "\n"; // Step 4
t.join();
}get_future()before the move. Why this step? Once westd::move(prom)in step 2 the localpromis empty; asking it for a future then would throw. Grab the reading end first.std::move(prom)into the thread. Why this step?std::promiseis move-only (see Move semantics and std::move); copying would create two writers to one shared state.set_value(6 * 7)after the sleep. Why this step? This writes the value , flips thereadyflag, and wakes any waiter oncv— exactly theset_valueblock in the parent's hand-built version.fut.get(). Why this step?mainreaches here well before 50 ms are up, so the shared state is not ready.get()putsmainto sleep on the condition variablecv(not a busy loop), and the CPU is free until step 3 wakes it.
Verify: the printed value is . Sanity on timing:
maincannot print before ~50 ms, proving it genuinely blocked rather than reading a stale zero.
Example 2 — Cell B: value set before the reader waits
Forecast: will
get()return instantly, hang forever, or throw?
std::promise<int> prom;
auto fut = prom.get_future();
prom.set_value(100); // ready flag flips to true HERE
int x = fut.get(); // called AFTER ready is already trueset_value(100)runs first. Why this step? It setsready = trueand callsnotify_all()oncv. There is no one waiting yet, so the notify wakes nobody — and that is fine.fut.get()runs second. Why this step? Recall the parent's predicatecv.wait(lk, []{ return ready; })— wherecvis the condition variable andlkis the lock the thread holds while sleeping (both named in the overview above). Becausereadyis already true, the predicate is satisfied before sleeping, sowaitreturns immediately without ever touchingcv. A missed notify is harmless — the predicate, not the signal, is the source of truth.
Verify:
x == 100, returned with zero blocking. This is precisely why the standard library uses a predicate wait and not a barewait(lk).
Example 3 — Cell C: transporting an exception across threads
Forecast: does the program crash, or does
maincatch the message?
void worker(std::promise<int> p) {
try {
throw std::runtime_error("disk on fire");
} catch (...) {
p.set_exception(std::current_exception()); // Step 1
}
}
int main() {
std::promise<int> prom;
auto fut = prom.get_future();
std::thread t(worker, std::move(prom));
try {
int x = fut.get(); // Step 2
std::cout << x;
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n"; // Step 3
}
t.join();
}set_exception(std::current_exception()). Why this step?std::current_exception()grabs the exception currently in flight and packages it as anexception_ptr. We store it in the shared state's exception slot instead of a value. (See Exception handling in C++.)fut.get(). Why this step? When the shared state holds an exception,get()does not return — it callsrethrow_exception, re-raising the same exception object here inmain's thread.- The
catch. Why this step? Because the exception was re-thrown at theget()call, an ordinarytry/catcharound it works. The thread boundary vanished.
Verify: output is exactly
caught: disk on fire, and the program does not terminate. Sanity check: only the exception slot was used —set_valuewas never called, so there is no half-written value.
Example 4 — Cell D: the broken promise
Forecast: does
get()hang forever (deadlock), or does it throw a specific error?
void worker(std::promise<int> p) {
// oops: no set_value, no set_exception — p is destroyed here
}
int main() {
std::promise<int> prom;
auto fut = prom.get_future();
std::thread t(worker, std::move(prom));
t.join();
try {
int x = fut.get(); // Step 2
std::cout << x;
} catch (const std::future_error& e) {
std::cout << e.code().value() << "\n"; // Step 3
}
}- Promise destroyed unset. Why this matters? The standard says: if a promise's shared state is abandoned (destructor runs with no value and no exception), the destructor automatically stores a
future_errorwith codebroken_promiseand marks the state ready. This is a safety net so the reader never deadlocks. fut.get(). Why this step? The state is ready (because of the auto-stored error), soget()unblocks — but it holds an exception, so it re-throwsfuture_error.- Catch and inspect
e.code(). Why this step? Confirms the error class isbroken_promise, not something else.
Verify:
get()throws, it does not hang. The library's guarantee: an unset promise still wakes its future, withbroken_promise. Every path in your producer must therefore eitherset_valueorset_exception.
Example 5 — Cell E: one-shot violations (two get()s, two get_future()s)
Forecast: for each abuse, which call throws and what does
fut.valid()read just before it?
// Abuse (a): two get_future() calls
std::promise<std::string> prom;
auto f1 = prom.get_future();
// auto f2 = prom.get_future(); // THROWS: future_already_retrieved
// Abuse (b): two get() calls
prom.set_value("hello");
std::cout << f1.valid() << "\n"; // Step 2 -> 1
std::string s = f1.get(); // Step 3 : moves the string out
std::cout << f1.valid() << "\n"; // Step 4 -> 0
// f1.get(); // Step 5 THROWS: no_state- Second
get_future()(abuse a). Why illegal? The shared state can be handed to one reading end. A second retrieval would create two futures fighting over one value, so it throwsfuture_already_retrieved. valid()before the firstget(). Why this step?valid()asks "am I still attached to a shared state?" Here it is1(true) — attached and unread.- First
get(). Why this step? It moves the string out of the shared state and detaches the future. valid()afterget(). Why this step? Now0(false) — the future has been consumed and holds no state.- Second
get()(abuse b). Why illegal? With no state left, there is nothing to return, so it throwsfuture_error(no_state). If you need many reads, convert to a shared_future (Example 8).
Verify:
valid()prints1then0, bracketing the single legalget(). It is the cheap flag that tells you whether aget()is still allowed.
Example 6 — Cell F: the zero / degenerate payload, promise<void>
Forecast: what does
set_value()look like with noT, and what doesget()return?
#include <future>
#include <thread>
#include <iostream>
int main() {
std::promise<void> done; // T is void: no storage slot
std::future<void> fut = done.get_future();
std::thread t([p = std::move(done)]() mutable {
// ... do setup work ...
p.set_value(); // Step 2 : no argument!
});
fut.get(); // Step 3 : returns void, just unblocks
std::cout << "proceeding\n";
t.join();
}promise<void>. Why this step? When there is no result, the value slot degenerates to nothing. Thereadyflag, mutexmand condition variablecvare still there — the signalling machinery is what we want, not the data.p.set_value()with empty parentheses. Why this step?voidhas no object to store, soset_valueis the pure "flip ready and notify" operation.fut.get()returningvoid. Why this step? It still blocks until ready, then returns nothing. This is a clean one-shot barrier between two threads — same guarantees as condition_variable but with the plumbing hidden.
Verify: conceptually,
promise<void>= the value cell removed, ready/mutex/cv retained."proceeding"prints only after the worker'sset_value(), proving the empty-payload signal still synchronizes.
Example 7 — Cell H: the limiting case, a deadline with wait_for
Forecast: after 100 ms of waiting, does
wait_forsayready,timeout, ordeferred?
using namespace std::chrono;
std::promise<int> prom;
auto fut = prom.get_future();
std::thread t([p = std::move(prom)]() mutable {
std::this_thread::sleep_for(milliseconds(300)); // slow producer
p.set_value(7);
});
auto status = fut.wait_for(milliseconds(100)); // Step 1
if (status == std::future_status::timeout) {
std::cout << "gave up waiting\n"; // Step 2
} else if (status == std::future_status::ready) {
std::cout << fut.get() << "\n";
}
t.join();wait_for(100ms). Why this step? Unlikewait()(blocks unconditionally) orget()(blocks and consumes),wait_forblocks at most the given duration and returns afuture_status. It never consumes the value, so a laterget()is still allowed once ready.- Branch on
future_status::timeout. Why this step? After 100 ms the producer (300 ms) is not done, so the status istimeoutand we take the give-up branch — no crash, no hang.
Verify: with producer 300 ms and deadline 100 ms, , so
wait_forreturnstimeout. (Thereadybranch andget()==7would only fire if the deadline exceeded 300 ms.)
Example 8 — Cell G: many readers with shared_future
Forecast: what one conversion turns a single-use future into a broadcastable one, and can each thread call
get()?
std::promise<int> prom;
std::shared_future<int> shared = prom.get_future().share(); // Step 1
auto reader = [shared](int id) {
int v = shared.get(); // Step 3 : each reads OK
std::cout << "reader " << id << " saw " << v << "\n";
};
std::thread a(reader, 1), b(reader, 2), c(reader, 3);
prom.set_value(55); // Step 2
a.join(); b.join(); c.join();.share(). Why this step?future::share()moves the shared state into a std::shared_future, which is copyable and whoseget()is not one-shot — it returns aconst&/copy each call.set_value(55). Why this step? One producer still fulfils exactly once; the broadcast is on the reading side.- Three
get()calls. Why legal now? Each of the three copies of theshared_futurecan callget(); they all observe the same55. With a plainfuture, only the first would succeed and the others would throwno_state.
Verify: all three readers print
55; total of three successfulget()s (impossible with a plain one-shotfuture).
Example 9 — Cells I + J: real word problem, and the packaged_task twist
Forecast: what final number does main print, and how many futures are needed?
auto compute = [](std::promise<int> p, int a, int b) {
p.set_value(a * b);
};
std::promise<int> pr, pp;
auto f_risk = pr.get_future();
auto f_pnl = pp.get_future();
std::thread t1(compute, std::move(pr), 3, 100); // risk = 300
std::thread t2(compute, std::move(pp), 5, 40); // pnl = 200
int total = f_risk.get() + f_pnl.get(); // Step 3
std::cout << total << "\n";
t1.join(); t2.join();- Two promises, two futures. Why? Two independent results ⇒ two shared states running concurrently — that is Cell I's parallel-fan-out.
- Two threads compute simultaneously. Why? No data dependency between
riskandpnl, so they overlap. See std::thread for how each is launched. f_risk.get() + f_pnl.get(). Why this step?mainblocks on each in turn; total time ≈ the slower of the two, not their sum.
Verify: .
Now the exam twist (Cell J) — where is the promise when you use a std::packaged_task?
std::packaged_task<int()> task([]{ return 6 * 7; }); // task hides a promise inside
std::future<int> fut = task.get_future(); // future from the task itself
std::thread(std::move(task)).detach(); // running the task = fulfilling it
std::cout << fut.get() << "\n"; // -> 42- Why no explicit
promise? Apackaged_taskowns an internal promise. Invoking the task callsset_valuewith the return value (orset_exceptionif it throws) automatically. You interact only with the future. Contrast with std::async, which builds the whole promise/thread/future trio for you.
Verify: the packaged-task future yields , identical to Example 1 but with the promise machinery hidden inside the task.
Recall
Recall Which cell throws instead of blocking, and why?
Cell D (broken promise) ::: The promise's destructor auto-stores future_error(broken_promise) and marks ready, so get() throws rather than deadlocking.
Recall Why did Example 2's
get() not miss the notify?
Value-set-first ::: cv.wait(lk, []{return ready;}) checks the predicate before sleeping; since ready was already true it returned instantly — the missed notify is irrelevant.
Recall What turns a one-shot future into a broadcast one?
.share() ::: It moves the state into a copyable std::shared_future whose get() may be called many times, once per reader.
Recall Which call gives a status instead of consuming the value?
wait_for ::: Returns future_status::ready / timeout / deferred after at most the given duration, without invalidating the future.