logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·27.10.26
← Back to news

C++ futures part 1: The mental model

Original article
devblogs.microsoft.com|47 points|9 comments|by ibobev|Oct 19, 2026

C++ futures part 1: The mental model

Microsoft

Written by Raymond Chen on October 12th, 2026.

The C++ standard library provides a suite of interconnected tools for asynchronous programming:

  • std::promise
  • std::packaged_task
  • std::async
  • std::future
  • std::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: Sempty→set_value/exceptionSready→consumeSconsumedS_{empty} \xrightarrow{\text{set\_value/exception}} S_{ready} \xrightarrow{\text{consume}} S_{consumed}Sempty​set_value/exception​Sready​consume​Sconsumed​


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:

StateMeaning
nothingNo value or exception has been provided yet.
valueA valid result of type T is available.
exceptionAn error occurred and is stored as an exception_ptr.
consumedThe 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.

Raymond Chen


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 consumed enumerator in this example isn't strictly used in the same way in the real-world implementation.

mind blown heart

About the Author: Raymond Chen 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.

youtube