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.
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.