Skip to content
C++
Library
since C++11
Intermediate

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++11

A 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:

  1. The creator — constructs the task and calls get_future() once to obtain the associated future.
  2. The executor — receives the task by move and invokes it (typically on a different thread).
  3. The consumer — holds the future and calls get() to block until the result is available.

Syntax

cpp
#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

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

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

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

cpp
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 exited

Use 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 ways packaged_task cannot 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 by packaged_task.
  • std::thread — typically the transport for a packaged_task; packaged_task itself does not start a thread.