std::promise
Write-end of a one-shot, thread-safe channel; pairs with std::future to pass a value or exception across threads.
std::promise<T>since C++11A move-only, one-time write endpoint that stores a value or exception into a heap-allocated shared state retrievable by the corresponding std::future<T>.
Overview
std::promise<T> and std::future<T> form a single-producer, single-consumer rendezvous. The promise owns the write side; get_future() hands off the read side. Once the promise fulfills the shared state — either with a value via set_value() or with an exception via set_exception() — any thread blocking on future::get() unblocks and retrieves the result.
The shared state is heap-allocated and reference-counted between both endpoints. A std::promise is move-only; copying it is deleted. Each promise can produce exactly one future: a second call to get_future() throws std::future_error with std::future_errc::future_already_retrieved. Both types live in <future> (C++11).
The standard workflow:
- Construct
std::promise<T>on the producing side. - Extract
std::future<T>viaget_future()and pass it to the consumer. - Move the promise into the producer thread or callable.
- Call
set_value()orset_exception()exactly once. - Consumer calls
future::get(), blocking until the state is ready.
Promise vs. packaged_task vs. async
| Mechanism | Who controls fulfillment | Best for |
|---|---|---|
std::promise | you, manually | custom executors, event-driven code, manual signaling |
std::packaged_task | wraps a callable | adapting existing functions to return futures |
std::async | the runtime | simple fire-and-forget or straightforward async calls |
Syntax
#include <future>
std::promise<T> p; // primary template; T must be MoveConstructible
std::promise<T&> p; // reference specialisation
std::promise<void> p; // signaling with no payload
std::future<T> f = p.get_future(); // call at most once
p.set_value(v); // fulfil with a value
p.set_exception(eptr); // fulfil with an exceptionMember function summary:
| Member | Description |
|---|---|
get_future() | Returns the associated future; throws on second call |
set_value(v) | Stores value; unblocks future::get() |
set_exception(eptr) | Stores exception; future::get() rethrows it |
set_value_at_thread_exit(v) | Defers fulfillment until current thread exits |
set_exception_at_thread_exit(eptr) | Defers exception storage until thread exits |
The _at_thread_exit variants ensure thread-local destructors finish before the consumer observes the result.
Examples
Basic producer–consumer handoff
#include <future>
#include <numeric>
#include <thread>
#include <vector>
#include <iostream>
int main() {
std::promise<long long> sum_promise;
std::future<long long> sum_future = sum_promise.get_future();
std::thread worker([p = std::move(sum_promise)]() mutable {
std::vector<int> data(100'000);
std::iota(data.begin(), data.end(), 1);
long long result = std::accumulate(data.begin(), data.end(), 0LL);
p.set_value(result);
});
std::cout << "Sum: " << sum_future.get() << '\n'; // blocks until ready
worker.join();
}Exception propagation
set_exception stores any exception pointer; future::get() rethrows it on the consumer's thread with the original type intact.
#include <future>
#include <thread>
#include <stdexcept>
#include <iostream>
void safe_divide(std::promise<double> p, double num, double den) {
try {
if (den == 0.0) throw std::domain_error("division by zero");
p.set_value(num / den);
} catch (...) {
p.set_exception(std::current_exception());
}
}
int main() {
std::promise<double> p;
auto f = p.get_future();
std::thread t(safe_divide, std::move(p), 10.0, 0.0);
try {
std::cout << f.get() << '\n';
} catch (const std::domain_error& e) {
std::cerr << "Caught: " << e.what() << '\n'; // "Caught: division by zero"
}
t.join();
}Void promise as a one-shot signal
std::promise<void> carries no payload — it functions as a lightweight notification primitive, replacing a condition_variable + bool flag with less ceremony.
#include <future>
#include <thread>
#include <iostream>
void do_work(std::future<void> ready) {
ready.wait(); // block until signaled
std::cout << "Worker running.\n";
}
int main() {
std::promise<void> gate;
auto f = gate.get_future();
std::thread worker(do_work, std::move(f));
// ... perform setup ...
std::cout << "Setup complete.\n";
gate.set_value(); // no argument for void specialisation
worker.join();
}Thread-pool task submission
std::promise is the right primitive when fulfillment is decoupled from a single callable — for example, in a hand-rolled executor:
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <thread>
#include <type_traits>
struct Pool {
std::queue<std::function<void()>> tasks;
std::mutex mu;
std::vector<std::thread> workers;
template <typename F, typename R = std::invoke_result_t<F>> // C++17
std::future<R> submit(F f) {
auto p = std::make_shared<std::promise<R>>();
auto fut = p->get_future();
{
std::lock_guard lk(mu); // C++17: CTAD
tasks.push([p, f = std::move(f)]() mutable {
try {
p->set_value(f());
} catch (...) {
p->set_exception(std::current_exception());
}
});
}
return fut;
}
};Best Practices
Move the promise; never share it. Promises are move-only for a reason. Pass ownership with std::move into the thread or lambda at construction time. Passing by reference invites lifetime hazards if the owning thread exits before fulfillment.
Always fulfill. If a promise is destroyed without set_value or set_exception, its destructor writes a std::future_error with std::future_errc::broken_promise into the shared state. The consumer's future::get() then throws silently corrupting your application logic. Gate all exit paths — normal returns, early returns, and exceptions — so exactly one fulfillment call occurs.
Prefer std::async for simple cases. std::async wires up promise and future automatically and handles the thread lifetime. Reach for std::promise only when fulfillment is genuinely decoupled from the callable that does the computation — custom schedulers, coroutine adapters, or event loops.
Use set_value_at_thread_exit for thread-local cleanup ordering. When the producing thread owns thread-local objects whose destructors must complete before the consumer proceeds, the _at_thread_exit variant guarantees that ordering without extra synchronization.
Fan out with std::shared_future. std::promise maps one-to-one with one future. When multiple threads must observe the same result, call future::share() to obtain a std::shared_future<T> that can be copied freely and get()-d from any number of threads.
Common Pitfalls
Calling get_future() twice. Each promise yields exactly one future. A second call throws std::future_error(future_errc::future_already_retrieved). Obtain the future before moving the promise, and hold only one copy.
Setting the value more than once. set_value and set_exception are one-shot. A second call throws std::future_error(future_errc::promise_already_satisfied). Ensure a single code path reaches the set call, or guard it with an atomic flag.
Capturing the promise by reference in a lambda. Passing a std::promise& to std::thread keeps ownership on the launching thread. If it exits before the worker calls set_value, the promise destructs first and the future receives broken_promise. Always capture by move: [p = std::move(promise)]() mutable { ... }.
Heap allocation cost. The shared state is heap-allocated per promise. In tight dispatch loops — millions of short tasks per second — this becomes measurable. Consider pooling promises, using lock-free queues, or C++20 coroutines (co_await) where allocation amortization matters.
See Also
std::future/std::shared_future— read endpoints of the shared state;shared_futureallows multiple consumersstd::packaged_task— wraps any callable and exposes a future without manual promise managementstd::async— highest-level abstraction; appropriate for most single-result async patternsstd::condition_variable— lower-level notification primitive; more control at the cost of explicit mutex management