Implementing Simple Futures with Coroutines

Instead of return, a coroutine uses co_return returning its result. In this post, I want to implement a simple coroutine using co_return.

You may wonder: Although I have presented the theory behind coroutines, I want to write once more about coroutines. My answer is straightforward and based on my experience. C++20 does not provide concrete coroutines. Instead, C++20 provides a framework for implementing coroutines. This framework consists of more than 20 functions, some of which you must implement and others can override. Based on these functions, the compiler generates two workflows, which define the behavior of the coroutine. To make it short. Coroutines in C++20 are double-edged swords. On one side, they give you enormous power; on the other side, they are pretty challenging to understand. I dedicated over 80 pages to coroutines in my book “C++20: Get the Details“, which has not yet explained everything.

From my experience, using simple coroutines and modifying them is the easiest – maybe only – way to understand them. And this is precisely the approach I’m pursuing in the following posts. I present simple coroutines and modify them. To make the workflow obvious, I put many comments inside and added only so much theory necessary to understand the internals of coroutines. My explanations are not complete and should only serve as a starting point to deepen your knowledge about coroutines.

A Short Reminder

While you can only call a function and return from it, you can call a coroutine, suspend and resume it, and destroy a suspended coroutine.

FunctionsVersusCoroutines

With the new keywords co_await and co_yield, C++20 extends the execution of C++ functions with two new concepts.

Thanks to co_await expression it is possible to suspend and resume the execution of the expression. If you use co_await expression in a function func, the call auto getResult = func() does not block if the function call’s result is unavailable. Instead of resource-consuming blocking, you have resource-friendly waiting.

co_yield expression supports generator functions. The generator function returns a new value each time you call it. A generator function is a data stream from which you can pick values. The data stream can be infinite. Therefore, we are at the center of lazy evaluation with C++.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    Additionally, a coroutine does not return its result, a coroutine does co_return  its result.

    // ...
    
    MyFuture<int> createFuture() {
        co_return 2021;
    }
    
    int main() {
    
        auto fut = createFuture();
        std::cout << "fut.get(): " << fut.get() << '\n';
    
    }   
    

    In this straightforward example createFuture is the coroutine because it uses one of the three new keywords co_return, co_yield, or co_await and it returns a coroutine MyFuture<int>. What? This is what often puzzled me. The name coroutine is used for two entities. Let me introduce two new terms. createFuture is a coroutine factory that returns a coroutine object fut, that can be used to ask for the result: fut.get().

    This theory should be enough. Let’s talk about co_return.

    co_return

    Admittedly, the coroutine in the following program eagerFuture.cpp is the simplest coroutine, I can imagine that still does something meaningful: it automatically stores the result of its invocation.

    // eagerFuture.cpp
    
    #include <coroutine>
    #include <iostream>
    #include <memory>
    
    template<typename T>
    struct MyFuture {
        std::shared_ptr<T> value;                           // (3)
        MyFuture(std::shared_ptr<T> p): value(p) {}
        ~MyFuture() { }
        T get() {                                          // (10)
            return *value;
        }
    
        struct promise_type {
            std::shared_ptr<T> ptr = std::make_shared<T>(); // (4)
            ~promise_type() { }
            MyFuture<T> get_return_object() {              // (7)
                return ptr;
            }
            void return_value(T v) {
                *ptr = v;
            }
            std::suspend_never initial_suspend() {          // (5)
                return {};
            }
            std::suspend_never final_suspend() noexcept {  // (6)
                return {};
            }
            void unhandled_exception() {
                std::exit(1);
            }
        };
    };
    
    MyFuture<int> createFuture() {                         // (1)
        co_return 2021;                                    // (9)
    }
    
    int main() {
    
        std::cout << '\n';
    
        auto fut = createFuture();
        std::cout << "fut.get(): " << fut.get() << '\n';   // (2)
    
        std::cout << '\n';
    
    }
    

    MyFuture behaves as a future, which runs immediately (see “Asynchronous Function Calls“). The call of the coroutine createFuture (line 1) returns the future, and the call fut.get (line 2) picks up the result of the associated promise.

    There is one subtle difference to a future: the return value of the coroutine createFuture is available after its invocation. Due to the lifetime issues of the coroutine, the coroutine is managed by a std::shared_ptr (lines 3 and 4). The coroutine always uses std::suspend_never (lines 5 and 6) and, therefore, neither does suspend before it runs nor after. This means the coroutine is immediately executed when the function createFuture is invoked. The member function get_return_object (line 7) returns the handle to the coroutine and stores it in a local variable. return_value (lines 8) stores the result of the coroutine, which was provided by co_return 2021 (line 9). The client invokes fut.get (line 2) and uses the future as a handle to the promise. The member function get finally returns the result to the client (line 10).

    eagerFuture

    You may think it is not worth implementing a coroutine that behaves just like a function. You are right! However, this simple coroutine is an ideal starting point for writing various implementations of futures.

    At this point, I should add a bit of theory.

    The Promise Workflow

    When you use co_yield, co_await, or co_return in a function, the function becomes a coroutine, and the compiler transforms its function body into something equivalent to the following lines.

    {
      Promise prom;                      // (1)
      co_await prom.initial_suspend();   // (2)
      try {                                         
        <function body>                  // (3)
      }
      catch (...) {
        prom.unhandled_exception();
      }
    FinalSuspend:
      co_await prom.final_suspend();     // (4)
    }
    

    Do these function names sound familiar to you? Right! These are the member functions of the inner class promise_type. Here are the steps the compiler performs when it creates the coroutine object as the return value of the coroutine factory createFuture. It first creates the promise object (line 1), invokes its initial_suspend member function (line 2), executes the body of the coroutine factory (line 3), and finally, calls the member function final_suspend (line 4). Both member functions initial_suspend and final_suspend in the program eagerFuture.cpp return the predefined awaitables std::suspend_never.  As its name suggests, this awaitable suspends never; hence, the coroutine object suspends never and behaves such as a usual function. An awaitable is something you can await on. The operator co_await needs an awaitable. I will write a post about the awaitable and the second awaiter workflow.

    From this simplified promise workflow, you can deduce which member functions the promise (promise_type) at least needs:

    • A default constructor
    • initial_suspend
    • final_suspend
    • unhandled_exception

    Admittedly, this was not the full explanation but at least enough to get the first intuition about the workflow of coroutines.

    What’s next?

    You may already guess it. In my next post, I will use this simple coroutine as a starting point for further experiments. First, I add comments to the program to make its workflow explicit, second, I make the coroutine lazy and resume it on another thread.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

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