Exercises — std - promise and std - future
This page is a drill sheet for the parent topic. If a term feels unfamiliar, that topic builds it from zero first.
Level 1 — Recognition
These test whether you can name and point at the right piece.
L1.1
Which object is the writing end and which is the reading end of the one-shot mailbox?
Recall Solution
- Writing end (producer):
std::promise<T>— you callset_value/set_exceptionon it. (This is the pen on the left of the top-of-page figure.) - Reading end (consumer):
std::future<T>— you callget()/wait()on it. (This is the key on the right of that figure.)
Both point at the same shared state box. Mnemonic: a promise is something you make (write); a future is something you wait for (read).
L1.2
Fill in the single call that links the two ends together:
std::promise<int> p;
std::future<int> f = p.__________;Recall Solution
std::future<int> f = p.get_future();get_future() hands you the reading key to the same shared state box (the dashed white arrow in the top-of-page figure). Call it exactly once — a second call throws std::future_error, because a single writer must not hand out two independent reading ends to one box.
L1.3
For each line, say whether it belongs to the producer or the consumer:
set_value · get · set_exception · wait · get_future
Recall Solution
Producer (promise) ::: set_value, set_exception, get_future
Consumer (future) ::: get, wait
(get_future lives on the promise but produces the consumer object.)
Level 2 — Application
Now you use the API in tiny complete snippets.
L2.1
A worker computes 9 * 9. Complete worker so the future receives 81.
void worker(std::promise<int> p) {
int r = 9 * 9;
// ???
}Recall Solution
void worker(std::promise<int> p) {
int r = 9 * 9;
p.set_value(r); // stores 81 in the box, marks ready, wakes the waiter
}This is the pen writing into the box in the top-of-page figure. fut.get() on the other side then returns 81.
L2.2
Order these four lines correctly so main compiles and runs. t.join() is not one of the four lines — assume it is already written on the line after the last of your four (you only reorder A–D):
A: std::thread t(worker, std::move(prom));
B: std::promise<int> prom;
C: std::future<int> fut = prom.get_future();
D: int x = fut.get();
Recall Solution
Correct order of the four lines: B, C, A, D (with the pre-existing t.join(); following D).
std::promise<int> prom; // B: make the promise (creates the box)
std::future<int> fut = prom.get_future();// C: grab the reading key BEFORE moving
std::thread t(worker, std::move(prom)); // A: move-only, hand promise to thread
int x = fut.get(); // D: block until set_value -> value
t.join(); // (given, not one of A–D)Why C before A: std::move(prom) transfers the promise's ownership of the box into the thread; the local prom is left empty (no shared state). If you then call prom.get_future(), there is no box to hand a key to, so it throws future_error(no_state). Getting the key before the move captures it while the local promise still owns the box. Line D is the key reading from the box in the top-of-page figure.
L2.3
This code deadlocks-or-crashes at runtime. What is the value printed / thrown, and why?
std::promise<int> prom;
auto fut = prom.get_future();
std::thread t([p = std::move(prom)]() mutable {
/* forgot to set anything */
});
t.join();
int x = fut.get(); // what happens here?Recall Solution
fut.get() throws std::future_error with error code broken_promise.
The lambda captured the promise by move and let it be destroyed without set_value/set_exception. The promise's destructor, seeing the box was never filled, marks the box ready but with a broken_promise error, then wakes waiters — so the waiting get() cannot deliver a value and re-throws instead of hanging forever.
Fix: set the promise on every path — e.g. p.set_value(0); inside the lambda.
Level 3 — Analysis
Reason about behaviour, ordering, and correctness.
L3.1
Explain why future::get() may only be called once, and what a second call does. Then give the standard tool for the "many readers" case.
Recall Solution
get() moves out the stored value (see Move semantics and std::move): after it returns, the box no longer holds a value for this future, and fut.valid() becomes false. A second get() therefore has nothing to return and throws std::future_error(no_state).
For multiple consumers, use std::shared_future (see std::shared_future): its get() returns a const reference (it copies out rather than moves out), so it can be called repeatedly by many copies.
L3.2
In the hand-built shared state, the reader first locks the mutex with a std::unique_lock, then waits on the condition variable. Why is the wait written
cv.wait(lk, []{ return ready; }) rather than plain cv.wait(lk)? Name two distinct problems the predicate solves.
Recall Solution
First, what is lk? It is the lock object holding the mutex:
std::unique_lock<std::mutex> lk(m); // lk owns the lock on mutex m
cv.wait(lk, []{ return ready; }); // cv atomically releases lk while sleeping,
// re-acquires it on wake, then checks readycv.wait needs a unique_lock (not a lock_guard) precisely because it must unlock the mutex while the thread sleeps and re-lock it on wake — see std::mutex and lock_guard.
Two problems the predicate solves (both from std::condition_variable):
- Spurious wakeups — a
condition_variablemay return fromwaitwith nonotify. The predicate re-checksreadyand goes back to sleep if it's still false. - Lost wakeup / set-before-wait race — if
set_valueruns before the reader reacheswait,readyis alreadytrue, so the predicate form returns immediately instead of blocking forever waiting for anotifythat already fired.
Plain cv.wait(lk) handles neither.
L3.3
Trace the states of fut.valid() at the three marked points:
std::promise<std::string> prom;
auto fut = prom.get_future(); // (A)
prom.set_value("hi"); // (B)
std::string s = fut.get(); // (C)Recall Solution
(A) after get_future() ::: true — associated with a shared state
(B) after set_value ::: true — value stored, but future not yet consumed
(C) after get() ::: false — value moved out, future invalidated
So the sequence is true, true, false. Setting the value does not invalidate the future; only consuming it does.
Level 4 — Synthesis
Combine promise/future with neighbouring tools.
L4.1
Rewrite this raw-thread pattern using std::promise/std::future so main receives the result without a global flag, mutex, or condition_variable of your own:
int shared_result;
bool ready = false; // hand-rolled sync — replace all of this
std::mutex m; std::condition_variable cv;Recall Solution
#include <future>
#include <thread>
#include <iostream>
void worker(std::promise<int> p) {
p.set_value(123); // one call packages storage + notify
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(worker, std::move(prom));
std::cout << fut.get() << "\n"; // -> 123
t.join();
}The single promise/future pair replaces the mutex, the std::condition_variable, the ready flag, and the shared variable — all bundled correctly inside the shared state.
L4.2
Ship an exception across the thread boundary. Worker throws std::out_of_range("bad index"); main must catch it at get() and print caught: bad index.
Recall Solution
void worker(std::promise<int> p) {
try {
throw std::out_of_range("bad index");
} catch (...) {
p.set_exception(std::current_exception()); // capture & ship
}
}
int main() {
std::promise<int> prom;
auto fut = prom.get_future();
std::thread t(worker, std::move(prom));
try {
int x = fut.get(); // re-throws here, in THIS thread
std::cout << x;
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n"; // -> caught: bad index
}
t.join();
}See Exception handling in C++. Without this, an exception escaping a thread function calls std::terminate.
L4.3
std::async gives you a future without ever touching a promise. Rewrite L4.1 using std::async and std::launch so no explicit promise, thread, or join appears.
Recall Solution
#include <future>
#include <iostream>
int main() {
std::future<int> fut = std::async(std::launch::async,
[]{ return 123; });
std::cout << fut.get() << "\n"; // -> 123
}std::async internally creates the promise/shared-state, runs the callable, sets the value (or exception) for you, and hands back the future.
Careful about the destructor nuance: a future returned by std::async is special — when it is destroyed it will block until the task finishes, but only if the task was launched with std::launch::async and is still running. With std::launch::deferred the callable has not started, and the deferred task runs only when you explicitly call get() or wait() — the future's destructor does not run it; if you never call get()/wait(), the callable simply never executes. If you omit the launch policy (std::async(f)), the implementation may pick either policy, so you cannot rely on either the blocking or the deferred behaviour. Only std::launch::async guarantees the "join on destruction" behaviour used above. (Futures obtained from a plain std::promise do not block on destruction.)
Level 5 — Mastery
Build and reason about the machinery itself.
L5.1
Implement a minimal MiniPromise<T> / MiniFuture<T> pair sharing a hand-built state, supporting set_value, set_exception, a blocking get(), and write-once enforcement. List the members the shared state must contain and why each exists.
Recall Solution
Members and their jobs (this is exactly the box drawn at the top of the page):
T value— storage for the successful result (the "value" inner slot in the figure).std::exception_ptr ex— an alternative slot so a thrown exception can be shipped instead of a value.bool ready = false— the reader-facing flag: "has the box been filled yet?" The consumer'scv.waitsleeps until this becomestrue(the "ready" inner slot).bool written = false— the writer-facing guard used for write-once enforcement: "has a producer already calledset_value/set_exception?" We need it separately fromreadybecause aset_*call must be able to detect a second write attempt (and throw) —readyalone can't distinguish "filled, waiting to be read" from "someone is trying to write a second time." In realstd::promisethis guard is what raisespromise_already_satisfied.std::mutex m— protectsvalue/ex/ready/writtenfrom a data race across threads (the "mutex" inner slot).std::condition_variable cv— lets the reader sleep and be woken, instead of busy-spinning (the "condition_variable" inner slot).
#include <mutex>
#include <condition_variable>
#include <exception>
#include <memory>
#include <stdexcept>
template<class T>
struct State {
std::mutex m;
std::condition_variable cv;
bool ready = false; // reader flag: has the box been filled?
bool written = false; // writer guard: has ANY set_* already happened?
T value{};
std::exception_ptr ex;
};
template<class T> struct MiniFuture; // forward declaration
template<class T>
struct MiniPromise {
std::shared_ptr<State<T>> s = std::make_shared<State<T>>();
MiniFuture<T> get_future(); // defined below, after MiniFuture
void set_value(T v) {
{ std::lock_guard<std::mutex> lk(s->m);
if (s->written) throw std::logic_error("already satisfied");
s->value = std::move(v);
s->written = true; // write-once guard
s->ready = true; // wake the reader
}
s->cv.notify_all();
}
void set_exception(std::exception_ptr e) {
{ std::lock_guard<std::mutex> lk(s->m);
if (s->written) throw std::logic_error("already satisfied");
s->ex = e;
s->written = true;
s->ready = true;
}
s->cv.notify_all();
}
};
template<class T>
struct MiniFuture {
std::shared_ptr<State<T>> s;
T get() {
std::unique_lock<std::mutex> lk(s->m); // unique_lock: wait can unlock it
s->cv.wait(lk, [&]{ return s->ready; }); // sleep until filled
if (s->ex) std::rethrow_exception(s->ex);// exception transport
return std::move(s->value); // move the value out
}
};
// now MiniFuture is complete, so we can define get_future():
template<class T>
MiniFuture<T> MiniPromise<T>::get_future() {
return MiniFuture<T>{ s }; // hand the SAME shared state to the future
}The two flags do different jobs: written enforces write-once on the producer side (throwing on a second set_*), while ready is the predicate the consumer waits on. The ex slot provides exception transport, and get_future() simply copies the shared_ptr so promise and future co-own one State — matching what real std::promise/std::future do.
L5.2
A thread pool runs 3 independent tasks returning ints 10, 20, 30. Collect all results using a std::vector<std::future<int>> and sum them. What is the sum, and why is the order of push_back irrelevant to correctness?
Recall Solution
std::vector<std::future<int>> futs;
futs.push_back(std::async(std::launch::async, []{ return 10; }));
futs.push_back(std::async(std::launch::async, []{ return 20; }));
futs.push_back(std::async(std::launch::async, []{ return 30; }));
int sum = 0;
for (auto& f : futs) sum += f.get(); // each get() blocks for ITS task
// sum == 60The sum is 60. Each get() waits independently for its own shared state, so whatever order the tasks finish in, the loop simply blocks on each in turn and adds it. Addition is commutative, so the total is order-independent.
L5.3
Explain, in terms of the shared state's ownership, why destroying an unfulfilled promise makes the future's get() throw broken_promise instead of blocking forever. Which design choice makes deadlock impossible here?
Recall Solution
The promise and future co-own the box (via a shared control block). When the promise is destroyed, its destructor checks: "did I ever set a value/exception?" If not, it stores a broken_promise error into the box and marks it ready — then wakes waiters.
So the waiting get() sees ready == true, finds a stored future_error(broken_promise), and re-throws it. The key design choice: "promise destruction always resolves the box (with a value, an exception, or a broken-promise error)", which guarantees the future can never wait on a promise that will never answer — no deadlock.
Recall Quick self-check summary
- Writing end = promise, reading end = future; both point at one shared state box.
get_future()beforestd::move(prom).- Exactly one write (
set_valueorset_exception). - Exactly one read (
get()), which invalidates the future. - Unset-then-destroyed promise →
broken_promiseatget().