Design and evolution of C++ future continuations

TL;DR: C++ futures lack continuation

“Continuation” is a rather standard concept in concurrent programming. JavaScript has promise.then, .NET has Task.ContinueWith. I was somewhat surprised that standard C++ std::future has no concept of “continuation” as of C++ 23.

  • Boost version sort of has it, but only if you turn on an undocumented #define.
  • It has been “experimental” for 11+ years, but not for lack of trying.
  • Multiple proposals were put forward, withdrawn, and re-proposed, but none has been accepted as of C++ 23. The earliest it can appear is C++ 26, but that’s not certain. I don’t want to be judgmental here, but the word “dysfunctional” is not leaving my mind.

But what about coroutines?

It’s a very good question. Coroutines (co_await/co_return) are by definition continuations, so they solve this problem, but in a completely different plane. Any code that uses futures would have to undergo significant rewrite to use co-routines. This may be a subject of another post.

std::future

Standard std::future does not have a then method, and never had. So, if I have a function returning a future, there is no way to say “call this other function returning a future, and when it’s done, do something else”.

future<string> get_text() {...}

class Parser {
   promise<MyData> m_promise;

   MyData parse_test_to_data() { ... };
public:

   future<MyData> get_parsed_data() {
    // obtain raw text, then parse it into data
    return get_text().then( // NOT AVAILABLE IN STANDARD C++!
       [this](const string& text) { m_promise.set_value(parse_text_to_data(text)); 
    }
}

Furthermore, std::future<T> is not convertible to future of any other type:

  • std::future<T> is not convertible to std::future<void>
  • std::future<TDerived> is not convertible to std::future<TBase>
  • std::future<short> is not convertible to std::future<int>

The reason for this is related: it would be trivial to implement via “wait, and then convert”, but std::future has no “then” facility.

boost::future::then

This method exists in a semi-zombie state:

  • It was added as “experimental” in 2012.
  • It is not included in the default code body of the library.
  • It is hidden behind an undocumented #define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION.
  • Boost documentation marks it as “extension” and adds a no-nonsense warning:
    These functions are experimental and subject to change in future versions. There are not too much tests yet, so it is possible that you can find out some trivial bugs 🙁

The #define part makes using the method especially problematic. If my code lives in a library, I cannot frivolously turn on arbitrary #defines, unless the entire ecosystem has agreed on it. Otherwise, my code and the client code would effectively use different flavors of boost, and the best I can hope for is quick and painless death by segmentation fault. This makes configuring header-only libraries via defines problematic in general, but it’s a subject for another post.

The road to hell is paved with good proposals

P0159, Concurrency TS Proposal: 2015-2019

The “concurrency TS” proposal, that would add future::then to the C++ standard was put forward in October 2015: P0159r0. It was probably not the first one, but anyhow, it was over 8 years ago. That proposals had been considered for over 3 years, only to see the std::future being withdrawn in January 2019 in favor of a “Facebook effort”. I suppose this has something to do with the Folly library, perhaps specifically Folly executors, but that’s not certain.

P0443, C++ Executors Proposal: 2016-2021

Eric Niebler, one of the authors of the “concurrency TS” explains why future::then is a bad idea in his CppCon 2019 talk. Thus, the “concurrency TS” proposal was rejected in favor of the “C++ executors proposal”, known as P0443. It was first put forward in October 2016 as P0443r0, with the last revision P0443r14 made in September 2020. Throughout 2021, there discussions about a “one grand unified model for asynchronous execution“, which resulted in the abandonment of P0443 in favor of P2300, the std::execution proposal.

This proposal calls for a free-standing then function that supersedes future::then and accepts a sender and a receiver.

P2300, std::execution proposal: 2021-present

This proposal was put forward in June 2021 as P2300r0 and underwent several revisions, the last one at the time of writing being P2300r7 from April 2023. It is based on P0443, but has significant design changes. Section 1.9, “Design changes from P0443″ states that “the executor concept has been removed and all of its proposed functionality is now based on schedulers and senders, as per SG1 direction“.

However, global then function remains virtually unchanged, see section 1.5.1.

P2300 did not make it into the C++23 standard. It might go into C++26, or it might not, if the C++ committee decides to make further refinements or to change the direction yet again.

Folly

Meta’s Folly library has its own nomenclature of concurrency-related facilities. It provides continuations via Future::thenValue method. Folly documentation gives the following example:

  folly::ThreadedExecutor executor;
  cout << "making Promise" << endl;
  Promise<int> p;
  Future<int> f = p.getSemiFuture().via(&executor);
  auto f2 = move(f).thenValue(foo);
  cout << "Future chain made" << endl;

Conclusion

Standard C++ is still lacking standardized future continuation. Boost has “experimental” implementation that is 11 years old. In the meantime, at least three major proposals were brought before the standard committee, but none made it into the standard. Current proposal is 190 printed pages long. The earliest it can become standard is C++26, and even that is not guaranteed. Such glacial speed of change results in a lot of confusion and feature lag behind other languages. C# had continuations since 2010, and JavaScript since 2015 (EC6). While C++ 11 was relatively on par with the industry when introducing futures, current C++ is quite behind the crowd. “Experimental”  code should not stick around for over a decade in any library, and especially in a semi-standard library like Boost. There must be a way to solve problems faster.

Posted in

Leave a Reply

Your email address will not be published. Required fields are marked *