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.

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.

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

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

  1. shared_ptr wrapping is necessary because std::function<void()> requires its target to be copy-constructible, and packaged_task is not. Heap-allocating through shared_ptr makes the lambda that captures it copyable.
  2. submit returns a typed std::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.

cpp
#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::exception types, are stored correctly via std::current_exception() / std::rethrow_exception().
  • If the task exits normally, the return value is stored instead.
  • If packaged_task itself is destroyed without ever being invoked, the shared state receives a std::future_error with broken_promise, which is re-thrown at get().

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.

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

cpp
worker: callable done, TLS still alive
TLS Cache flushed (value=42)
result: 42

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

Aspectstd::asyncstd::packaged_taskstd::promise
Abstraction levelHighestMiddleLowest
Starts a thread?Yes (or defers)No β€” you control the threadNo β€” value set manually
Thread controlNoneFull (which thread, when)Full
Value set whenFunction returnsFunction returns (or make_ready_at_thread_exit)Anywhere β€” even mid-function
Exception handlingAutomaticAutomaticManual (set_exception)
Reusable callableNoYes, via reset()No
Typical use caseOne-shot fire-and-forgetThread pools, work queues, deferred dispatchSignalling mid-computation; producer/consumer split

Decision guide:

  • Reach for std::async when you just want a result from a callable on another thread and don't care which thread runs it.
  • Reach for packaged_task when 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::promise when 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.

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

Questionstd::asyncstd::packaged_taskstd::promise
Who starts the thread?The runtimeYouYou (or nobody β€” value set in-place)
When is the value set?When callable returnsWhen callable returns (or at thread exit)At any point during execution
Callable required?YesYesNo β€” set_value anywhere
Callable reusable?NoYes, via reset()No
Exception handlingAutomaticAutomatic via operator()Manual: set_exception(std::current_exception())
Store in std::function?N/AOnly via shared_ptr wrapperYes (trivially copyable value)
Best forOne-shot fire-and-forgetPools, queues, deferred dispatchCallback 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.

cpp
// 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 by std::rethrow_exception() at get().
  • broken_promise β€” std::future_error with std::future_errc::broken_promise, stored when a valid but uninvoked packaged_task is destroyed. Any waiter on the associated future unblocks and receives this error.
  • promise_already_satisfied β€” std::future_error thrown synchronously (not stored) when operator() is called on a task that has already been invoked without an intervening reset().

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