5.2.28 · D1C++ Programming

Foundations — std - promise and std - future

3,152 words14 min readBack to topic

Before you can read the parent note, you must own every word it leans on. This page builds each one from zero — plain words first, then a picture, then why the topic needs it. Read top to bottom; each item is a brick for the next. The two named ends above are just labels for now; sections 3–6 build the machinery inside the box, and section 7 defines the ends precisely.


1. Thread — "a worker that runs at the same time"

Picture two runners on parallel tracks. They start together, run at their own pace, and neither waits for the other unless you tell them to.

Figure — std - promise and std - future

Figure 1 — the two arrows are the two threads (teal = main, orange = worker); the plum dots mark that both start at the same moment and then advance independently along their own track.

Why the topic needs it: promise/future exists only because more than one thread is running. If there were one thread, you'd just call a function and read its return value — no mailbox needed. See std::thread.


2. Shared data + the data race — "two hands on one object"

Picture two people writing on one sheet of paper at once: the letters overlap into nonsense.


3. Mutex + lock_guard + unique_lock — "the talking stick"

Picture a talking stick: only the person holding it may speak; everyone else waits.

std::mutex m;                          // the lock object itself
int shared_value;                      // the thing we protect
 
{
    std::lock_guard<std::mutex> lk(m); // lock m here (lk "guards" m)
    shared_value = 42;                 // safe: we hold the stick
}                                      // lk destroyed -> unlock automatically

Why the topic needs it: the shared state's flag and value are written by one thread and read by another — a mutex serialises those touches so there is no data race, and unique_lock is the exact tool the waiting step needs. See std::mutex and lock_guard.


4. The ready flag — "is the box filled yet?"

Picture a mailbox with a little red flag: the postman raises it once the letter is inside, so you know to walk out and collect.

Why the topic needs it: without it, the waiter cannot tell an empty box from a box holding a legitimately-zero value. The flag is the "data present" bit — and it is the condition the next tool (the condition variable) waits on.


5. Condition variable — "sleep until poked"

Picture someone napping with a note: "wake me when the kettle boils." They burn no energy waiting; whoever boils the kettle taps their shoulder — that tap is notify_one.

Figure — std - promise and std - future

Figure 2 — the teal box is the sleeping waiter holding a unique_lock; the orange box is the setter that has just stored the value and flipped ready; the plum arrow is the notify_one() call that wakes the waiter. Compare with the busy-wait note at the top: that would spin the teal box at 100% CPU instead of sleeping.

The alternative — a busy-wait loop while(!ready) {} (using the ready flag from section 4) — spins the CPU at 100% doing nothing. The condition variable is the efficient sleep.

Here is the canonical wait loop the setter and waiter perform, using the m, ready, and cv from the shared state:

std::mutex m;
std::condition_variable cv;
bool ready = false;
int value;
 
// --- SETTER (producer thread) ---
{
    std::lock_guard<std::mutex> lk(m); // 1. lock the mutex
    value = 42;                        // 2. store the value
    ready = true;                      // 3. flip the ready flag
}                                      // 4. unlock (lk destroyed)
cv.notify_one();                       // 5. wake the waiter (outside the lock)
 
// --- WAITER (consumer thread) ---
{
    std::unique_lock<std::mutex> lk(m);        // unique_lock, NOT lock_guard
    cv.wait(lk, []{ return ready; });          // sleeps; releases m while asleep,
                                               // re-checks ready on every wake
    int got = value;                           // safe: we hold m and ready==true
}

6. Shared state — "the mailbox all three live in"

Picture one physical mailbox with two keys — the writer's key and the reader's key — but only one box.

Figure — std - promise and std - future

Figure 3 — the central boxed block is the shared state; inside it are the four members from section 3–5 (value-or-exception, ready flag, mutex, condition variable). The orange arrow is the promise writing in; the teal arrow is the future reading out. Both arrows point at the same box — that is what "shared" means.

Why the topic needs it: this is literally the "one thing they jointly manage" the parent note names. Promise = writing end, future = reading end, shared state = the box between them.


7. The promise and the future — "the two ends of the box"

Picture the mailbox again: the promise is the slot on the street where the postman drops the letter; the future is the little door on your side of the wall where you collect it.


8. Exceptions and exception_ptr — "shipping an error in the box"

Why the topic needs it: this is set_exception / re-throw in get() — the parent's Worked Example 2.


9. Move semantics and std::move — "hand over, don't clone"

Figure — std - promise and std - future

Figure 4 — top row: copying a promise (the dashed clone, struck through in red) would make two writing ends aim at the same box, so two threads could both call set_value — the language forbids it. Bottom row: std::move transfers the single shared state from the old promise (now empty, dotted outline) to the new one (solid) — still exactly one owner. The picture is the rule: one box, one writer.

Why the topic needs it: std::thread t(worker, std::move(prom)); — you hand the promise to the worker; cloning it would be an error the language refuses to allow. See Move semantics and std::move.


10. One-shot semantics — "read the letter, then it's gone"

Picture opening a sealed envelope: once opened and the letter removed, there's nothing left to open again. A second get() throws future_error with code no_state.

Why the topic needs it: it explains why get() invalidates the future, why valid() flips to false, and why multiple readers need std::shared_future instead.


How these bricks build the topic

Thread runs code in parallel

Two threads share memory

Data race is undefined behaviour

Mutex plus unique_lock make access safe

Ready flag says value is present

Condition variable sleeps until notify_one

Shared state box holds value flag mutex cv

Exception plus exception_ptr ship errors

Move semantics one owner only

promise writes, future reads

One-shot read then invalid


Once these foundations are solid, the parent topic connects outward to higher-level tools that wrap the same machinery: std::async and std::launch (which hands you a future automatically), std::packaged_task (which wraps a callable's return in a promise), and std::shared_future (many readers of one result).


Equipment checklist

Test yourself — you should be able to answer each before reading the parent note.

What is a thread, in one line?
An independent stream of instructions that runs at the same time as others.
What is a data race and why is it dangerous?
Two threads touch the same memory with at least one writing — it is undefined behaviour (garbage, crash, or hidden bugs).
What does a mutex guarantee?
Only one thread at a time enters the protected region; others wait, so shared access is serialised and safe.
What does lock_guard do for you?
Locks the mutex on creation and unlocks it automatically at end of scope, so you never forget to release.
Why does condition_variable::wait need unique_lock rather than lock_guard?
Because wait must release the mutex while sleeping and re-acquire it on waking, which only unique_lock allows; lock_guard cannot unlock partway through and won't compile there.
What job does the ready flag do?
It records whether the value has been placed in the box, so the reader can tell an empty box from a filled one.
What are notify_one and notify_all?
Condition-variable calls that wake one waiting thread (notify_one) or every waiting thread (notify_all) after the condition changed.
In what order must the setter act, and where does notify go?
Lock the mutex, store the value, flip the ready flag, unlock, then call notify_one outside the lock (notifying while locked just makes the woken thread block again).
Why must a condition-variable wait re-check its condition in a loop?
Because of spurious wakeups (waking with no notify) and the case where the value was set before waiting began — wait(lk, pred) is exactly that re-checking loop.
Why is the handoff actually safe (memory-ordering)?
Unlocking a mutex happens-before the next lock of the same mutex, so all writes made before unlock (value and ready) are visible after the waiter re-locks inside wait.
Why use a condition variable instead of a busy-wait loop?
It lets the waiting thread sleep at zero CPU cost until it is signalled, instead of spinning uselessly.
What is the shared state?
A heap-allocated control block holding the value-or-exception, ready flag, mutex, and condition variable, shared by promise and future.
What is a std::promise and what is a std::future?
The promise is the writing end (set_value or set_exception once); the future is the reading end (get() blocks then reads once).
What is std::future_error?
The exception type (from , derived from logic_error) thrown on future misuse; it carries a future_errc code like broken_promise, future_already_retrieved, or no_state.
What happens if a promise is destroyed without being set?
The future's get() throws a std::future_error with code broken_promise.
How does an exception cross a thread boundary?
Capture it as a std::exception_ptr with current_exception(), store it, then rethrow_exception in the other thread.
Why is std::promise move-only?
A shared state must have exactly one writer; copying would create two writing ends to one box.
What does one-shot mean here?
The promise is set once and the future is read once; get() moves the value out and invalidates the future.
When would you use std::shared_future instead of std::future?
When several threads must all read the same result, since shared_future is copyable and its get() can be called many times.