packaged_task and promise

A future is one end of a channel, and std::promise is the other. The promise lives where the result is produced, the future where it is consumed, and either thread may set or get it.

std::async of lesson 110 builds the pair for a function call. When the producer is a queue, a thread pool or an event loop, the two ends are made by hand: a promise for a value, or a packaged_task for a function that already knows how to report.

The promise is also the tool for a value that one thread produces and several read: a std::shared_future is made from a future, and the promise stays the single writer. The shared state is the object that carries the value and the readiness between them.

The two ends

promise.get_future() gives the reading end, and set_value writes the value into the shared state. The future can be moved to another thread before the value exists, and get waits for it.

The program makes the pair, passes the promise to a worker by reference and reads the future in main. The value is 42, and the order of the two prints is fixed by the get.

A promise is move-only and exactly one value crosses it. It is the smallest possible channel: no queue, no buffer, one result and, optionally, one exception.

#include <future> #include <iostream> #include <thread> int main() { std::promise<int> promise; std::future<int> result = promise.get_future(); std::thread worker{[&promise] { promise.set_value(42); }}; std::cout << result.get() << '\n'; worker.join(); }

g++ -std=c++23 -Wall -Wextra -pthread promise.cpp -o promise
./promise

42

A packaged_task

A std::packaged_task<int(int, int)> wraps a callable with that signature and creates the promise internally. get_future is its reading end, and calling the task with its arguments runs the function and stores the result.

The program wraps add, takes the future, moves the task into a thread and reads 42. The task is move-only because the promise inside it is, which is why the thread constructor gets std::move(task).

This is the shape a thread pool needs: the pool owns a queue of packaged_tasks, the caller keeps the futures, and a worker runs each task and fills in the value. Lesson 112 builds exactly that.

#include <future> #include <iostream> #include <thread> int add(int a, int b) { return a + b; } int main() { std::packaged_task<int(int, int)> task{add}; std::future<int> result = task.get_future(); std::thread worker{std::move(task), 20, 22}; std::cout << result.get() << '\n'; worker.join(); }

g++ -std=c++23 -Wall -Wextra -pthread packaged.cpp -o packaged
./packaged

42

An exception through the promise

set_exception stores an exception in the shared state, and the future rethrows it on get. The argument is a std::exception_ptr, which is what std::current_exception() returns inside a catch block.

The program's worker throws, catches everything and calls set_exception(current_exception()). Main catches the logic_error and prints its message, so the exception crossed the thread boundary without a shared variable.

The catch-all is deliberate: any exception the worker can throw has to be converted, because an exception that leaves the thread function terminates the program. The same rule applies to every thread body in this part of the course.

#include <future> #include <iostream> #include <stdexcept> #include <thread> int main() { std::promise<int> promise; std::future<int> result = promise.get_future(); std::thread worker{[&promise] { try { throw std::logic_error{"no answer"}; } catch (...) { promise.set_exception(std::current_exception()); } }}; try { std::cout << result.get() << '\n'; } catch (const std::logic_error& error) { std::cout << "caught: " << error.what() << '\n'; } worker.join(); }

g++ -std=c++23 -Wall -Wextra -pthread exception.cpp -o exception
./exception

caught: no answer

Two tasks, two futures

The pair is made per task, so a program that starts several tasks keeps a future for each. The program wraps two calls, runs them on two threads and adds the results.

The futures are read in the order the program wants, not the order the tasks finish. The sum is 25 whether the square of 3 or the square of 4 comes back first, and the joins happen after the reads.

That decoupling is the value of the design: the queue of tasks and the list of futures can have different orders, and a thread pool is free to run them in any order it likes.

#include <future> #include <iostream> #include <thread> int square(int value) { return value * value; } int main() { std::packaged_task<int()> first{[&] { return square(3); }}; std::packaged_task<int()> second{[&] { return square(4); }}; std::future<int> a = first.get_future(); std::future<int> b = second.get_future(); std::thread t1{std::move(first)}; std::thread t2{std::move(second)}; std::cout << a.get() + b.get() << '\n'; t1.join(); t2.join(); }

g++ -std=c++23 -Wall -Wextra -pthread many.cpp -o many
./many

25

The broken promise

When a promise is destroyed without set_value or set_exception, its future can never receive anything. The standard calls that state a broken promise, and get throws std::future_error with the code future_errc::broken_promise.

The program lets the promise go out of scope and then reads the future. The print is std::future_error: Broken promise, which names exactly what happened.

The same error appears when a packaged_task is destroyed without ever being called. A task that is dropped from a queue still has to be run or have its exception set, or its caller waits forever for a value that cannot come.

#include <future> #include <iostream> int main() { std::future<int> result; { std::promise<int> promise; result = promise.get_future(); } try { std::cout << result.get() << '\n'; } catch (const std::future_error& error) { std::cout << error.what() << '\n'; } }

g++ -std=c++23 -Wall -Wextra -pthread broken.cpp -o broken
./broken

std::future_error: Broken promise

Exercises

  1. Use a promise to send a std::string from a worker and print its length in main.
  2. Wrap a function that throws in a packaged_task and read the exception through its future.
  3. Start three packaged_tasks and print the sum of their results in reverse order of the starts.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is the relationship between a promise and a future?

    Answer

    The promise is the writing end and the future is the reading end of one shared state

    get_future connects the two, and one value or exception crosses from the promise to the future.

  2. What does promise.get_future() return?

    Answer

    The future that will receive the value

    It is called once, and the returned future is the reading end of that promise.

  3. What does a std::packaged_task combine?

    Answer

    A callable and the promise that will receive its result

    Calling the task runs the callable and stores its result or exception in the promise it owns.

  4. Why is a packaged_task moved into the thread rather than copied?

    Answer

    Because it owns a promise, so it is move-only

    The promise inside makes the task non-copyable, and std::move transfers it to the thread.

  5. What does this program print? #include <future> #include <iostream> #include <thread> int main() { std::promise<int> promise; std::future<int> result = promise.get_future(); std::thread worker([&promise] { promise.set_value(42); }); std::cout << result.get(); worker.join(); }

    Answer

    42

    The worker sets 42 and the get in main waits for it.

  6. What does this program print? #include <future> #include <iostream> #include <thread> int add(int a, int b) { return a + b; } int main() { std::packaged_task<int(int, int)> task{add}; std::future<int> result = task.get_future(); std::thread worker(std::move(task), 20, 22); std::cout << result.get(); worker.join(); }

    Answer

    42

    The task calls add with 20 and 22, and the future carries 42.

  7. What does this program print? #include <future> #include <iostream> #include <stdexcept> #include <thread> int main() { std::promise<int> promise; std::future<int> result = promise.get_future(); std::thread worker([&promise] { try { throw std::logic_error{"no answer"}; } catch (...) { promise.set_exception(std::current_exception()); } }); try { std::cout << result.get(); } catch (const std::logic_error& error) { std::cout << "caught: " << error.what(); } worker.join(); }

    Answer

    caught: no answer

    The exception is stored by the promise and rethrown by get, where the catch prints the message.

  8. What does this program print? #include <future> #include <iostream> #include <thread> int square(int value) { return value * value; } int main() { std::packaged_task<int()> first{[&] { return square(3); }}; std::packaged_task<int()> second{[&] { return square(4); }}; std::future<int> a = first.get_future(); std::future<int> b = second.get_future(); std::thread t1(std::move(first)), t2(std::move(second)); std::cout << a.get() + b.get(); t1.join(); t2.join(); }

    Answer

    25

    The futures are read in program order, so 9 and 16 are added and the sum is 25.

  9. What does get throw when the promise was destroyed without a value?

    Answer

    std::future_error with the code broken_promise

    The shared state is marked broken and the error code names the reason.

  10. What does set_exception take?

    Answer

    A std::exception_ptr, such as the result of std::current_exception()

    The exception_ptr type is the copyable handle that carries an exception between threads.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image