5.2.28C++ Programming

std - promise and std - future

2,097 words10 min readdifficulty · medium

WHY does this exist?

The thing they jointly manage is called the shared state — a heap-allocated control block holding (value | exception), a ready flag, and synchronization primitives.


WHAT are they, precisely?


HOW it works under the hood (derivation from first principles)

We don't memorize the API — we reconstruct it. What must a correct "send one value between threads" gadget contain?

  1. Storage for the result. Type T, plus an alternative exception_ptr slot.
  2. A ready flag bool ready = false. WHY: the reader must know if data is present.
  3. A mutex m. WHY: writing the value/flag and reading it from two threads is otherwise a data race (undefined behavior).
  4. A condition_variable cv. WHY: the reader must sleep (not busy-spin) until ready, and be woken on set.

Putting it together, a minimal hand-built version:

template<class T>
struct SharedState {
    std::mutex m;
    std::condition_variable cv;
    bool ready = false;
    T value;
    std::exception_ptr ex;
};
 
// promise.set_value(v):
{
    std::lock_guard<std::mutex> lk(state->m);
    state->value = v;          // store
    state->ready = true;       // mark
}                              // unlock
state->cv.notify_all();        // wake the future
 
// future.get():
{
    std::unique_lock<std::mutex> lk(state->m);
    state->cv.wait(lk, [&]{ return state->ready; }); // sleep until ready
    if (state->ex) std::rethrow_exception(state->ex);
    return std::move(state->value);
}

This is exactly what the standard library std::promise/std::future do for you — correctly, portably, and with exception transport built in.

Figure — std - promise and std - future

Worked Example 1 — basic value transfer

#include <future>
#include <thread>
#include <iostream>
 
void worker(std::promise<int> p) {
    int result = 6 * 7;          // pretend this is heavy work
    p.set_value(result);         // fulfil the promise
}
 
int main() {
    std::promise<int> prom;          // Why: create the writing end
    std::future<int>  fut = prom.get_future(); // Why: grab reading end BEFORE moving promise
 
    std::thread t(worker, std::move(prom)); // Why: promise is move-only, must std::move
 
    std::cout << fut.get() << "\n";  // Why: blocks here until worker calls set_value -> 42
    t.join();
}
  • Why get_future() before std::move(prom)? After moving prom into the thread, the local prom is empty; you can no longer get the future from it.
  • Why std::move(prom)? std::promise is non-copyable (a shared state can't be duplicated). Only one owner of the writing end.

Worked Example 2 — transporting an exception

void worker(std::promise<int> p) {
    try {
        throw std::runtime_error("disk on fire");
        // p.set_value(1); // never reached
    } catch (...) {
        p.set_exception(std::current_exception()); // Why: capture & ship the exception
    }
}
 
int main() {
    std::promise<int> prom;
    auto fut = prom.get_future();
    std::thread t(worker, std::move(prom));
    try {
        int x = fut.get();   // Why: this re-throws the runtime_error in THIS thread
        std::cout << x;
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << "\n"; // -> caught: disk on fire
    }
    t.join();
}
  • Why is this powerful? An exception thrown in another thread normally calls std::terminate. promise/future lets you move the exception across the thread boundary and handle it where you wait.

Worked Example 3 — using valid() and one-shot semantics

std::promise<std::string> prom;
auto fut = prom.get_future();
prom.set_value("hello");
 
std::cout << fut.valid() << "\n";  // 1  (still associated)
std::string s = fut.get();         // moves the string out
std::cout << fut.valid() << "\n";  // 0  (consumed! get() invalidated it)
// fut.get(); // Why illegal: would throw future_error(no_state)
  • Why does get() invalidate? It moves out the result; a second get() has nothing to return, so the standard forbids it (one-shot channel).

Common Mistakes (Steel-man + fix)


Flashcards

What links a promise to its future?
A shared (heap-allocated) shared state holding value/exception, a ready flag, mutex and condition_variable.
How do you obtain the future from a promise?
prom.get_future() — call exactly once (twice throws future_error).
Why must you std::move a promise into a thread?
std::promise is move-only (non-copyable); only one owner of the writing end is allowed.
What does future::get() do?
Blocks until the shared state is ready, then moves out the value (or re-throws the stored exception), and invalidates the future.
How do you transport an exception across threads?
In the producer, p.set_exception(std::current_exception()); the consumer's get() re-throws it.
What happens if a promise is destroyed without being set?
The future's get() throws std::future_error with code broken_promise.
Why use a predicate in cv.wait(lk, pred)?
To handle spurious wakeups and the case where the value was set before waiting began (re-checks ready).
Difference between future::wait() and future::get()?
wait() blocks until ready but does not consume; get() blocks then consumes (moves out) the value.
When use std::shared_future?
When multiple threads must read the same result (it's copyable and get() can be called many times).
How many times may a promise be set?
Exactly once; a second set_value/set_exception throws promise_already_satisfied.

Recall Feynman: explain to a 12-year-old

Imagine you order a pizza by phone. The cook gives you a paper receipt (the future) and keeps the kitchen ticket (the promise). You go sit down with your receipt; you can't eat yet, so you wait. When the pizza is ready, the cook uses the ticket to put your pizza on the counter and rings a bell. Your receipt lets you pick it up — exactly one pizza, one time. And if the oven catches fire, the cook can hand you a note saying "sorry, no pizza" instead — and you'll find out the moment you check your receipt. The receipt+ticket pair makes sure you never grab a pizza that isn't ready and never grab the same one twice.


Connections

  • std::thread — the worker that runs concurrently and fulfils the promise.
  • std::async and std::launch — a higher-level wrapper that returns a future for you.
  • std::packaged_task — wraps a callable + an automatic promise.
  • std::shared_future — multi-consumer version of future.
  • std::condition_variable — the primitive future/promise are built on top of.
  • std::mutex and lock_guard — protects the shared state.
  • Exception handling in C++exception_ptr, current_exception, rethrow_exception.
  • Move semantics and std::move — why promises/futures are move-only.

Concept Map

solved by

get_future links to

jointly manage

jointly manage

calls

calls

marks

waits on

contains

contains

contains

prevents

sleeps until

enables

re-throws via

Thread A needs result from Thread B

std promise T - writing end

std future T - reading end

shared state control block

set_value or set_exception once

get blocks then reads once

mutex

condition variable

ready flag

value or exception_ptr

exception transport

data race

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Socho ek thread (Producer) koi heavy calculation kar raha hai, aur dusra thread (Consumer) us result ka wait kar raha hai. Problem ye hai ki std::thread apne creator ko value return nahi kar sakta, aur agar tum khud global variable + flag use karo to mutex aur condition_variable manually likhna padta hai — easily race condition ho jaati hai. Yahan std::promise aur std::future aata hai: ye dono ek "mailbox" ke do ends hain. Promise likhne wala end hai, future padhne wala end. Beech mein ek shared state hota hai jisme value, exception, ready-flag, mutex aur cond_var sab packed hote hain.

Flow simple hai: pehle prom.get_future() se future nikaalo, phir promise ko std::move karke worker thread ko de do (promise copy nahi ho sakta, sirf move). Worker jab ready ho to p.set_value(result) call karta hai. Consumer thread mein jab tum fut.get() likhte ho, wo thread block ho jaata hai jab tak value set nahi hoti — busy-waiting nahi, proper sleep. Value set hote hi consumer jaag jaata hai aur value ko move-out karke return kar deta hai. Yaad rakho: ek baar set, ek baar get — ye one-shot channel hai.

Sabse zabardast feature: agar producer thread mein exception aa jaye, normally program std::terminate ho jaata. Lekin tum p.set_exception(std::current_exception()) karke us exception ko dusre thread tak bhej sakte ho, aur consumer ka get() usko wahin re-throw kar dega — tum normal try/catch se handle kar loge. Yani error bhi safely thread boundary cross karta hai.

Common galtiyan: promise ko copy karne ki koshish (move karo), future ko move ke baad nikaalna (pehle nikaalo), aur get() ko do baar call karna (ek hi baar allowed, warna future_error). Aur agar promise bina set kiye destroy ho gaya to broken_promise exception milega — isliye har code path par value ya exception zaroor set karo. Mantra: "Promise Pushes, Future Fetches."

Go deeper — visual, from zero

Test yourself — C++ Programming

Connections