C++ futures part 1: The mental model
C++ futures part 1: The mental model

Written by Raymond Chen on October 12th, 2026.
The C++ standard library provides a suite of interconnected tools for asynchronous programming:
std::promisestd::packaged_taskstd::asyncstd::futurestd::shared_future
At their core, all these components are wrappers around a single, unnamed internal class that the standard refers to as the "state." For the sake of clarity, I will refer to this as the source. If you are familiar with C#, this source behaves very similarly to a TaskCompletionSource.
The Conceptual Framework
The source is the engine that produces the eventual value. Conceptually, it must handle several key tasks:
- Store a resulting value or an exception.
- Provide a mechanism to signal that the value is ready.
- Allow consumers to wait for the result.
- Ensure the value is retrieved safely (and potentially only once).
State Transitions
We can represent the state of the source using a simple mathematical transition:
Implementing the Source
Below is a simplified implementation of this source (omitting reference support for brevity).
template <typename T>
struct source {
private:
enum what { nothing, value, exception, consumed } d;
// A union to hold either the result or the error
union {
T value;
std::exception_ptr ex;
} r;
std::mutex m;
std::condition_variable cv;
The d variable (the discriminant) tracks the current status of the union:
| State | Meaning |
|---|---|
nothing | No value or exception has been provided yet. |
value | A valid result of type T is available. |
exception | An error occurred and is stored as an exception_ptr. |
consumed | The value was already retrieved and is no longer available. |
The std::mutex ensures that access is thread-safe, while the std::condition_variable allows threads to block until the state changes.
Ensuring Single Assignment
To prevent multiple values from being set, we use a helper method:
void must_be_empty() {
if (d != what::nothing) {
throw std::future_error(future_errc::promise_already_satisfied);
}
}
Setting the Result
You can provide a value or an exception. Both operations notify any waiting threads.
public:
void set_value(T v) {
std::lock_guard g(m);
must_be_empty();
new(&r.value) T(v); // Placement new to initialize the union member
d = what::value;
cv.notify_all();
}
void set_exception(std::exception_ptr ex) {
std::lock_guard g(m);
must_be_empty();
new(&r.ex) std::exception_ptr(ex);
d = what::exception;
cv.notify_all();
}
Note: I've omitted the T&& overload, but it functions identically using std::move().
Waiting for Completion
The source supports both indefinite waiting and timed waiting.
void wait() {
std::lock_guard g(m);
while (d == what::nothing) {
cv.wait(g);
}
}
void wait_for(duration dur) {
std::lock_guard g(m);
cv.wait_for(g, dur, [ ] { return d != what::nothing; });
}
The wait_for functionality is the primary reason for using a condition_variable. Without it, we could have simply used the wait and notify_all methods found in std::atomic.
Retrieving the Value
Before accessing the data, we must verify that the value hasn't been taken and that no exception was thrown.
void can_get() {
if (d == what::consumed) {
throw std::future_error(future_errc::future_already_retrieved);
}
if (d == what::exception) {
std::rethrow_exception(r.ex);
}
}
T consume() {
wait();
std::lock_guard g(m);
can_get();
d = what::consumed;
return std::move(r.value);
}
const T get() {
wait();
std::lock_guard g(m);
can_get();
return r.value;
}
};
Visualizing the Flow
Final Thoughts
Bonus chatter: I'll leave the destructor implementation as an exercise for you. It's not particularly difficult, but dealing with the union makes it a bit tedious.
Bonus bonus chatter: I am writing this series because I frequently forget the inner workings of futures; documenting it is my way of cementing the knowledge.
Community Discussion
Nathan Phillip Brink (Binki): Noticed a small typo: "values an exceptions" should be "values and exceptions."
LB:
It is worth noting that in actual practice, the standard library doesn't typically throw future_already_retrieved. While it exists in the enum, std::future::get usually throws no_state if the value was already retrieved. Consequently, the enumerator in this example isn't strictly used in the same way in the real-world implementation.consumed
About the Author:
Raymond Chen has spent over three decades helping evolve Windows. In 2003, he started The Old New Thing, a blog that became unexpectedly popular (a fact that still makes him nervous). He also wrote a book of the same name and occasionally shares anecdotes on Twitter.