std::packaged_task
A move-only callable wrapper that stores its result or exception in a shared state accessible via std::future.
std::packaged_tasksince C++11A move-only wrapper around a callable that, when invoked, stores the return value or propagated exception into a shared state retrievable through an associated std::future.
Overview
std::packaged_task<R(Args...)>, declared in <future> (C++11), is one of three mechanisms for producing a shared state alongside std::promise and std::async(). It occupies the middle ground: more explicit than async(), less ceremonial than wiring a promise by hand.
The mental model is straightforward. A packaged_task wraps a callable (function, lambda, functor) and ties it to a shared state. Calling the taskβpassing the appropriate argumentsβexecutes the callable and either stores its return value in the shared state or, if the callable throws, stores the exception there instead. A std::future<R> obtained beforehand can then retrieve either outcome.
Because the task owns the shared state, it is move-only. Copying would create two tasks that each believe they control the same future, which the type system prevents.
Three parties interact with a packaged_task:
- The creator β constructs the task and calls
get_future()once to obtain the associated future. - The executor β receives the task by move and invokes it (typically on a different thread).
- The consumer β holds the future and calls
get()to block until the result is available.
Syntax
#include <future>
template<class R, class... Args>
class packaged_task<R(Args...)> {
public:
packaged_task() noexcept; // C++11 β empty, !valid()
explicit packaged_task(F&& f); // C++11 β wrap callable f
packaged_task(packaged_task&&) noexcept; // C++11 β move-only
packaged_task(const packaged_task&) = delete;
bool valid() const noexcept; // C++11
std::future<R> get_future(); // C++11 β call once
void operator()(Args... args); // C++11 β invoke, set result
void make_ready_at_thread_exit(Args... args); // C++11 β defer until thread exits
void reset(); // C++11 β new shared state, reuse callable
void swap(packaged_task& other) noexcept; // C++11
};The function signature in the template parameter must exactly match how the callable will be invoked. packaged_task<int(int, int)> wraps any callable that accepts two ints and returns int.
Examples
Basic thread launch
#include <future>
#include <thread>
#include <cstdlib>
int gcd(int a, int b) {
while (b) { a %= b; std::swap(a, b); }
return a;
}
int main() {
std::packaged_task<int(int, int)> task(gcd);
std::future<int> result = task.get_future();
// task must be moved β it is not copyable
std::thread t(std::move(task), 56, 98);
t.detach();
// blocks until gcd(56, 98) is stored in the shared state
int answer = result.get(); // answer == 14
}Deferred task queue
packaged_task is the natural building block for thread pools and work queues because it decouples enqueuing from execution.
#include <deque>
#include <future>
#include <functional>
#include <numeric>
#include <thread>
#include <vector>
int partial_sum(int beg, int end) {
int s = 0;
for (int i = beg; i < end; ++i) s += i;
return s;
}
int main() {
constexpr int N = 100'000;
constexpr int chunks = 4;
std::deque<std::packaged_task<int()>> queue;
std::vector<std::future<int>> futures;
for (int i = 0; i < chunks; ++i) {
int lo = (N / chunks) * i;
int hi = (N / chunks) * (i + 1);
// bind args into a nullary callable β packaged_task<int()>
std::packaged_task<int()> t([lo, hi]{ return partial_sum(lo, hi); });
futures.push_back(t.get_future());
queue.push_back(std::move(t));
}
// drain the queue across detached threads
while (!queue.empty()) {
std::thread worker(std::move(queue.front()));
queue.pop_front();
worker.detach();
}
int total = 0;
for (auto& f : futures) total += f.get();
// total == 0+1+...+99999 == 4'999'950'000 / ... == 4999950000 / 2? check:
// partial_sum(0,N) returns N*(N-1)/2 = 4999950000 β correct
}Exception propagation
When the wrapped callable throws, packaged_task catches the exception and stores it in the shared state. Calling future::get() re-throws it in the consuming thread.
#include <future>
#include <stdexcept>
#include <thread>
double inverse(double x) {
if (x == 0.0) throw std::domain_error("division by zero");
return 1.0 / x;
}
int main() {
std::packaged_task<double(double)> task(inverse);
auto fut = task.get_future();
std::thread(std::move(task), 0.0).detach();
try {
double v = fut.get(); // re-throws std::domain_error
} catch (const std::domain_error& e) {
// handle in the calling thread
}
}make_ready_at_thread_exit
make_ready_at_thread_exit invokes the callable immediately but delays making the shared state ready until after the calling thread's thread-local destructors have run. This ensures consumers only see the result after all thread-local cleanup has completed.
std::packaged_task<int(int, int)> task(gcd);
auto fut = task.get_future();
std::thread([t = std::move(task)]() mutable {
t.make_ready_at_thread_exit(56, 98); // result visible only after thread exits
}).detach();
int v = fut.get(); // blocks until thread has fully exitedUse this when the callable modifies thread-local state that must be flushed before any observer reads the result.
Best Practices
Prefer packaged_task over raw promise for wrapping callables. promise requires you to manually call set_value or set_exception inside a try/catch; packaged_task handles the exception transport automatically.
Prefer std::async() for single, fire-and-forget asynchronous calls. Reserve packaged_task for scenarios where you need explicit control over scheduling β thread pools, work stealing, deferred queues, or cases where the same callable needs to be reset and reused with reset().
Call get_future() exactly once before moving the task. The future must be retrieved before the task can be dispatched; it cannot be retrieved after the task has moved.
Join or detach the thread before the future's destructor runs. The shared state keeps the callable result alive, but the thread must still be managed. Ignoring the std::thread handle causes std::terminate().
Common Pitfalls
Calling get_future() more than once throws std::future_error with future_already_retrieved. There is exactly one future per task.
Letting a valid task go out of scope uninvoked leaves the shared state without a stored value or exception. Any thread blocked on the associated future will remain blocked indefinitely β or receive std::future_error (broken_promise) when the last reference to the shared state is released.
Attempting to copy a packaged_task. It is explicitly non-copyable. Store tasks in containers using std::move or wrap them in a std::unique_ptr.
Reusing without reset(). Invoking a packaged_task a second time throws std::future_error (promise_already_satisfied). Call reset() first: this constructs a fresh shared state, and you must call get_future() again to obtain a new future for it.
Confusing valid() with readiness. valid() indicates whether the task owns a shared state, not whether a result has been stored. A newly constructed task is valid(); a moved-from task is not.
Thread Pool Pattern
packaged_task is the canonical building block for a thread pool that returns typed futures. The key insight is that packaged_task is move-only, so it cannot be stored directly in a std::function (which requires copyability). The standard solution is to wrap it in a shared_ptr, then capture that pointer inside a std::function<void()> that the worker queue holds.
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <type_traits>
#include <vector>
class ThreadPool {
public:
explicit ThreadPool(std::size_t n = std::thread::hardware_concurrency()) {
for (std::size_t i = 0; i < n; ++i)
workers_.emplace_back([this] { run(); });
}
~ThreadPool() {
{
std::unique_lock lock(mu_);
stop_ = true;
}
cv_.notify_all();
for (auto& t : workers_) t.join();
}
// Enqueue any callable; returns a future for the result.
template<typename F>
auto submit(F&& f) -> std::future<std::invoke_result_t<F>> {
using R = std::invoke_result_t<F>;
// packaged_task is move-only, so wrap in shared_ptr for the queue
auto task = std::make_shared<std::packaged_task<R()>>(std::forward<F>(f));
std::future<R> fut = task->get_future();
{
std::unique_lock lock(mu_);
queue_.emplace([task]{ (*task)(); });
}
cv_.notify_one();
return fut;
}
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
private:
void run() {
for (;;) {
std::function<void()> job;
{
std::unique_lock lock(mu_);
cv_.wait(lock, [this]{ return stop_ || !queue_.empty(); });
if (stop_ && queue_.empty()) return;
job = std::move(queue_.front());
queue_.pop();
}
job();
}
}
std::vector<std::thread> workers_;
std::queue<std::function<void()>> queue_;
std::mutex mu_;
std::condition_variable cv_;
bool stop_ = false;
};Usage:
int main() {
ThreadPool pool(4);
auto f1 = pool.submit([]{ return 6 * 7; });
auto f2 = pool.submit([]{ return std::string("hello"); });
int n = f1.get(); // 42
auto s = f2.get(); // "hello"
}Two design points confirmed by community sources:
shared_ptrwrapping is necessary becausestd::function<void()>requires its target to be copy-constructible, andpackaged_taskis not. Heap-allocating throughshared_ptrmakes the lambda that captures it copyable.submitreturns a typedstd::future<R>, so callers get compile-time type safety and exception propagation for free β the pool never needs to know about exceptions in individual tasks.
Exception Propagation in Depth
When operator() is called on a packaged_task and the wrapped callable throws, the exception is captured into the shared state β it is not propagated to the thread that invoked the task. The throwing thread continues normally. The exception surfaces only when the associated future's get() is called, at which point it is re-thrown in the calling thread.
#include <future>
#include <stdexcept>
#include <iostream>
#include <thread>
double safe_divide(double num, double den) {
if (den == 0.0)
throw std::domain_error("division by zero");
return num / den;
}
int main() {
std::packaged_task<double(double, double)> task(safe_divide);
std::future<double> fut = task.get_future();
// Run on a worker thread β exception is caught inside operator()
// and stored in the shared state; the thread itself does NOT crash.
std::thread worker(std::move(task), 1.0, 0.0);
worker.join(); // worker finished without throwing
try {
double v = fut.get(); // re-throws std::domain_error HERE
(void)v;
} catch (const std::domain_error& e) {
std::cerr << "caught: " << e.what() << '\n';
// output: caught: division by zero
}
}Key guarantees from the standard:
- Any exception derived from
std::exception, as well as arbitrary non-std::exceptiontypes, are stored correctly viastd::current_exception()/std::rethrow_exception(). - If the task exits normally, the return value is stored instead.
- If
packaged_taskitself is destroyed without ever being invoked, the shared state receives astd::future_errorwithbroken_promise, which is re-thrown atget().
In a thread pool context this means worker threads never need a try/catch around individual jobs β exceptions flow out through futures to whoever called get().
make_ready_at_thread_exit
make_ready_at_thread_exit executes the callable immediately β just like operator() β but delays marking the shared state as ready until after the calling thread's thread-local storage (TLS) destructors have run.
#include <future>
#include <iostream>
#include <thread>
// Simulate a thread-local cache that must be flushed before results are visible.
thread_local struct Cache {
int value = 0;
~Cache() { std::cout << "TLS Cache flushed (value=" << value << ")\n"; }
} tls_cache;
int compute() {
tls_cache.value = 42; // write into thread-local state
return tls_cache.value;
}
int main() {
std::packaged_task<int()> task(compute);
auto fut = task.get_future();
std::thread([t = std::move(task)]() mutable {
// callable runs now, but future is NOT ready yet
t.make_ready_at_thread_exit();
std::cout << "worker: callable done, TLS still alive\n";
// TLS destructors run here, then shared state becomes ready
}).join();
// By the time join() returns the shared state is guaranteed ready
std::cout << "result: " << fut.get() << '\n';
}Expected output:
worker: callable done, TLS still alive
TLS Cache flushed (value=42)
result: 42This ordering guarantee β confirmed by cppreference β means that by the time any waiter unblocks from future::get(), all TLS objects in the worker thread have already been destroyed. This is the correct tool when:
- The task modifies thread-local state (e.g. a per-thread allocator, logger, or connection cache) that must be committed before observers read the result.
- You need a clean shutdown sequence where cleanup is observable before the result propagates.
operator() does not provide this guarantee: the shared state is made ready inside the call, before TLS teardown.
Comparison: packaged_task vs promise vs async
All three produce a std::future, but they sit at different abstraction levels and suit different scenarios.
| Aspect | std::async | std::packaged_task | std::promise |
|---|---|---|---|
| Abstraction level | Highest | Middle | Lowest |
| Starts a thread? | Yes (or defers) | No β you control the thread | No β value set manually |
| Thread control | None | Full (which thread, when) | Full |
| Value set when | Function returns | Function returns (or make_ready_at_thread_exit) | Anywhere β even mid-function |
| Exception handling | Automatic | Automatic | Manual (set_exception) |
| Reusable callable | No | Yes, via reset() | No |
| Typical use case | One-shot fire-and-forget | Thread pools, work queues, deferred dispatch | Signalling mid-computation; producer/consumer split |
Decision guide:
- Reach for
std::asyncwhen you just want a result from a callable on another thread and don't care which thread runs it. - Reach for
packaged_taskwhen you need to decouple creation from execution β store tasks in a queue, assign them to specific threads, or reuse the same callable with fresh shared state. - Reach for
std::promisewhen the value is produced at an arbitrary point during execution rather than at the function's return β for example, signalling a condition that was verified partway through a long operation, or bridging a callback-based API to a future-based one.
The key distinction between packaged_task and promise (sourced from jakascorner.com): both async and packaged_task can only communicate a value at the end of a function call. promise can communicate a value at any point during execution β before the function even returns.
Dispatching packaged_task from a Queue to Worker Threads
A queue of packaged_task objects is the minimal primitive for typed, heterogeneous thread pools. Each task carries its own return type through the future it yields before being enqueued, so the worker threads only need to call operator() on a type-erased wrapper while callers hold strongly-typed std::future<R> handles.
The pattern below shows the essential mechanics without a full pool class: tasks are enqueued as shared_ptr-wrapped packaged_task objects (because std::function requires copyability), dispatched to waiting worker threads via a condition variable, and results retrieved through the futures collected before enqueue.
// C++11
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
std::queue<std::function<void()>> work_queue;
std::mutex queue_mu;
std::condition_variable queue_cv;
bool done = false;
// Worker thread body β type-blind, exception-safe
void worker_body() {
for (;;) {
std::function<void()> job;
{
std::unique_lock<std::mutex> lk(queue_mu);
queue_cv.wait(lk, []{ return done || !work_queue.empty(); });
if (done && work_queue.empty()) return;
job = std::move(work_queue.front());
work_queue.pop();
}
job(); // exceptions are captured inside packaged_task β worker never throws
}
}
// Enqueue helper β wraps callable in packaged_task, stores in queue, returns future
template<typename F, typename R = std::invoke_result_t<F>>
std::future<R> enqueue(F&& f) {
auto task = std::make_shared<std::packaged_task<R()>>(std::forward<F>(f));
std::future<R> fut = task->get_future();
{
std::unique_lock<std::mutex> lk(queue_mu);
work_queue.emplace([task]{ (*task)(); });
}
queue_cv.notify_one();
return fut;
}
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i)
workers.emplace_back(worker_body);
auto f1 = enqueue([]{ return 6 * 7; }); // std::future<int>
auto f2 = enqueue([]{ return std::string("ok"); });// std::future<std::string>
int r1 = f1.get(); // 42
std::string r2 = f2.get(); // "ok"
{ std::unique_lock<std::mutex> lk(queue_mu); done = true; }
queue_cv.notify_all();
for (auto& t : workers) t.join();
}Crucially, worker threads require no knowledge of R β the erasure happens at the enqueue call site, while type safety is preserved through the returned std::future<R>. Any exception thrown inside the callable is stored in the shared state and re-thrown only at f1.get() or f2.get(), keeping worker threads crash-free.
packaged_task vs std::async vs std::promise β Decision Guide
Choose the mechanism by asking who controls the thread and when the value is produced. The three future-producing primitives are not interchangeable: each trades off scheduling control against boilerplate and flexibility.
| Question | std::async | std::packaged_task | std::promise |
|---|---|---|---|
| Who starts the thread? | The runtime | You | You (or nobody β value set in-place) |
| When is the value set? | When callable returns | When callable returns (or at thread exit) | At any point during execution |
| Callable required? | Yes | Yes | No β set_value anywhere |
| Callable reusable? | No | Yes, via reset() | No |
| Exception handling | Automatic | Automatic via operator() | Manual: set_exception(std::current_exception()) |
Store in std::function? | N/A | Only via shared_ptr wrapper | Yes (trivially copyable value) |
| Best for | One-shot fire-and-forget | Pools, queues, deferred dispatch | Callback bridging, mid-computation signals |
Reach for std::async when you want a single asynchronous result and don't need to control which thread runs the work or when it is scheduled. Its destructor semantics (std::launch::async futures block on destruction) can surprise callers in tight loops, but for simple use it is the lowest-friction option.
Reach for packaged_task when creation and execution must be decoupled β the callable is constructed in one place (the submitting thread) and run in another (a worker). It is also the right choice when the same callable needs to be reset and re-run with a fresh shared state, or when you need the make_ready_at_thread_exit ordering guarantee.
Reach for std::promise when the value emerges at an arbitrary interior point of a computation rather than at the function's return boundary β for example, signalling the first match found during a parallel search, or wrapping a C callback that delivers a result asynchronously to a different code site than where the computation was initiated.
Exception Propagation Details
Exceptions thrown inside a packaged_task are silently transported to the future, not to the executing thread. This is the property that makes packaged_task safe to invoke from worker threads: the thread that calls operator() cannot observe the exception at all; only the thread that calls future::get() will encounter it.
// C++11
#include <future>
#include <iostream>
#include <stdexcept>
#include <thread>
int risky(int x) {
if (x < 0) throw std::underflow_error("negative input");
return x * x;
}
int main() {
std::packaged_task<int(int)> task(risky);
std::future<int> fut = task.get_future();
// Worker invokes with a bad argument β exception is stored, not propagated
std::thread worker(std::move(task), -1);
worker.join(); // worker exits cleanly β no crash, no terminate()
try {
int v = fut.get(); // std::underflow_error re-thrown here
(void)v;
} catch (const std::underflow_error& e) {
std::cerr << "consumer caught: " << e.what() << '\n';
}
}The standard specifies three exception sources a packaged_task future can carry:
- Callable exception β any type throwable by the wrapped function, stored via
std::current_exception()and re-thrown bystd::rethrow_exception()atget(). broken_promiseβstd::future_errorwithstd::future_errc::broken_promise, stored when a valid but uninvokedpackaged_taskis destroyed. Any waiter on the associated future unblocks and receives this error.promise_already_satisfiedβstd::future_errorthrown synchronously (not stored) whenoperator()is called on a task that has already been invoked without an interveningreset().
Understanding the difference between the first two cases matters in thread-pool teardown: if work remains in the queue when the pool is destroyed, queued-but-never-run tasks will deposit broken_promise errors into their futures. Callers must handle std::future_error from get() in addition to whatever the callable might throw.
See Also
std::promiseβ lower-level shared-state producer; use when the value and the computation site are decoupled in wayspackaged_taskcannot accommodate.std::asyncβ highest-level option; automatically selects a thread policy and returns a future without requiring explicit task management.std::future/std::shared_futureβ the consumer side of the shared state produced bypackaged_task.std::threadβ typically the transport for apackaged_task;packaged_taskitself does not start a thread.