October 12th, 2026
0 reactions

C++ futures part 1: The mental model

The C++ standard library has a group of related functions and classes: std::promise, std::packaged_task, std::async, std::future, and std::sharedfuture.

The idea is that all of these classes are built of a single unnamed class which the C++ standard library calls the “state”, but for the purpose of discussion, I’m going to call it the source, since it is the thing that produces the value, and it behaves analogously to a C# task completion source.

The source conceptually supports these operations:

  • Set a value.
  • Set an exception.
  • Wait for a value or exception to be set.
  • Retrieve the value or rethrow the exception.

Here’s a simplified version that doesn’t support references.

template<typename T>
struct source
{
private:
    enum what {
        nothing,
        value,
        exception,
        consumed,
    } d; // discriminant

    union {
        T value;
        std::exception_ptr ex;
    } r; // result

    std::mutex m;
    std::condition_variable cv;

The discriminant tells us what is in the union: Nothing (because nobody has set anything), a value, an exception, or nothing (because the value has been consumed).

The mutex ensures thread-safe access to the other members, and the condition variable lets us wait for somebody to set a value or exception.

    void must_be_empty() {
        if (d != what::empty) {
            throw std::future_error(future_errc::promise_already_satisfied);
        }
    }

You can call the set methods only when the source is empty, so we’ll factor out the emptiness check.

public:
    void set_value(T& v) {
        std::lock_guard g(m);
        must_be_empty();
        new(&r.value) T(v); // "r.value = v"
        d = what::value;
        cv.notify_all();
    }

You can set a value or exception only once, so if there is already a value or exception, we yell at you. Otherwise, we save the value and notify anybody who was waiting for a value to be set.

I won’t bother with the overload that takes T&&; it’s the same thing, but with a std::move().

There is also a set_value_at_thread_exit which will be another topic for another day.

Instead of setting a value, you can set an exception:

    void set_exception(std::exception_ptr ex) {
        std::lock_guard g(m);
        must_be_empty();
        new(&r.ex) std::exception_ptr(ex); // "r.ex = ex"
        d = what::exception;
        cv.notify_all();
    }

It’s basically the same as set_value except that we store an exception.

That’s how values an exceptions go into the source. The next part is waiting for the source to become nonempty.

    void wait() {
        std::lock_guard g(m);
        while (d == what::empty) {
            cv.wait(g);
        }
    }

    void wait_for(duration dur) {
        std::lock_guard g(m);
        cv.wait_for(g, dur, [&] { return d != what::empty; });
    }

You can either wait indefinitely or wait for a particular duration. The wait_for method is why we have a condition variable at all. If it weren’t for that guy, we could have used std::atomic‘s wait and notify_all methods.

The last part is getting the values out.

    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);
        }
    }

Regardless of how we intend to retrieve the value, it’s not legal to retrieve a value that has already been consumed. Also, if the result is an exception, we rethrow the exception.

    T consume() {
        wait();
        std::lock_guard g(m);
        can_get();
        d = what::consumed;
        return std::move(r.v);
    }

    const T& get() {
        wait();
        std::lock_guard g(m);
        can_get();
        return r.v;
    }
};

You can get the value, which returns a const reference to the underlying storage. Or you can consume the value, which moves the value out of the underlying storage and marks the value as consumed so nobody can consume or get it (because it’s not there any more).

We can now build our other objects and functions out of this guy. We’ll do that next time.

Bonus chatter: Writing the destructor is left as an exercise for the reader. It’s not hard, but it’s annoying because of the union.

Bonus bonus chatter: I’m writing the series because I always forget how it works, but writing it down helps me remember.

Topics

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Sort by :
  • Paul Herring 23 minutes ago
        
    enum what {
            nothing,
            value,
            exception,
            consumed,
        } d; // discriminant
    
    [...]
        void must_be_empty() {
            if (d != what::empty) {
    

    I’m missing something here; what’s the value of what::empty?

  • Richard Deeming 5 hours ago

    As a non-C++ dev, maybe I’m missing something, but it seems odd that the methods to read a value are protected by the same std::lock_guard g(m) as the methods to write a value.

    In C# at least, something like that would lead to a deadlock: the reading thread is blocked waiting for the value to be set, and the writing thread is blocked waiting for the reading thread to release the mutex.

    But as I said, maybe I’m missing something?

    • Yexuan Xiao 1 hour ago

      This is basically the idiomatic usage of C++ condition variables, a mechanism designed to handle spurious wakeups of the mutex while keeping exception safety. Also, as Pavel Vazharov said, Raymond used std::lock_guard incorrectly.

    • Pavel Vazharov 4 hours ago

      I think that the answer for you doubts is in the behavior of std::condition_variable::wait.
      It takes a reference to std::unique_lock and atomically calls lock.unlock() and blocks waiting. (I think the article has a mistake in the wait member functions where it uses std::lock_guard. It should be std::unique_lock.