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
- Use a promise to send a std::string from a worker and print its length in main.
- Wrap a function that throws in a packaged_task and read the exception through its future.
- 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.
-
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.
-
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.
-
What does a
std::packaged_taskcombine?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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
What does
getthrow when the promise was destroyed without a value?Answer
std::future_errorwith the code broken_promiseThe shared state is marked broken and the error code names the reason.
-
What does
set_exceptiontake?Answer
A
std::exception_ptr, such as the result ofstd::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