Friday, December 10, 2021

C++ coroutine_traits: short notes

Understanding reality. It’s not about personal growth. It shouldn’t be. It’s trying to understand how reality works at a deep level. It’s not that we want the worlds to be there; it’s just the simplest, most austere way of understanding the data.

--Wired Magazine, Interview with Sean Carrol: https://www.wired.com/story/sean-carroll-thinks-we-all-exist-on-multiple-worlds/

A slight digression

Before continuing on the next post which will be about co_await, awaitable/awaiter interfaces and scheduling of coroutines across threads, I would like to implement a small variant of the previous blog entry.

In this entry I make a small dig into decoupling the future object (coroutine return object) from the promise_type. Well, at least decoupling as far as referencing the promise_type directly in the future class.

Looking up the promise_type using coroutine_traits

In the previous post I specified the promise type inside the future class as:

template<typename T>
struct Gen{
  using promise_type=val_promise_type<Gen>;
  ...
};

What if I don't want to explicitly name the promise_type inside the future? Can I do that? Yup, that is doable within the framework of C++20 coroutines.

A C++ traits class is simply a lookup table even though a rather convoluted one, where types are looked up using types as keys. A std::coroutine_traits class pointpoints a promise_type given the name of a future (coroutine return object) together with the parameter types to the coroutine.

In our example, the promise type is Gen<T> and the coroutine has the following signature where T is of type int:

Gen<int>genint(int n);

To lookup the corresponding future in the std::coroutine_traits the compilation machinery will use the following specialization we write for std::coroutine_trait:

// traits for looking up the promise object from a Gen<T> type
template <typename T, typename... Args>
struct std::coroutine_traits<Gen<T>,Args...>{
  class promise_type{
    ...
  };
};

The specialization is wider than needed since it allows for any number of coroutine parameters of unspecified types. In our case, we could have written the trait simply as:

template <>
struct std::coroutine_traits<Gen<int>,int>{
  class promise_type{
    ...
};
};

or as this if we want to force one parameter to the coroutine but leave the parameter type unspecified:

template <typename T>
struct std::coroutine_traits<Gen<T>,T>{
  class promise_type{
    ...
  };
};

From here on, I will stay with the last std::coroutine_traits specification.

The complete code

To summerize I'll show the complete code here. There are some noticeable differences from the code in the previous blog post that I will point out.

The first difference is that the promise_type is not explicitly mentioned in the future (Gen<T> class). Instead, the compilation machinery finds the promise_type by looking it up inside the std::coroutine_traits class that we defined.

A consequence of that we don't know the promise_type inside the future is that we cannot call a method on the promise_type<void> to retrieve the yield value. That is, we cannot retrieve the promise_type object by calling promise() on the coroutine handle. Instead, in the promise_type where the future is created, we pass as a second parameter to the future constructor a reference to the value stored inside the promise_type. Now, we can access the value stored inside the promise_type object.

This may not look too elegant, but it does solve our problem of not knowing the structure of the promise_type from inside the future when we need to get hold of the value.

The C++ documentation states that coroutine_handle<void> erases the promise_type. It should be noted that the coroutine_handle<void> is still a perfectly good handle for controlling the coroutine., With the handle we can resume the coroutine, or we can check if the coroutine completed (using done()) without knowing anything about the promise_type.

#include <iostream>
#include <vector>
#include <optional>
#include <coroutine>
using namespace std;

// future that does not know the promise_type
// (promise_type is looked up by the compiler through the std::coroutine_traits<Gen<T>,T>)
template<typename T>
class Gen{
public:
  // ctor, dtor
  Gen(coroutine_handle<void>handle,T const&valref):handle_(handle),valref_(valref){}
  ~Gen(){handle_.destroy();}

  // get current value
  // (we have a reference to the last yield value in the promise object)(
  T value()const{return valref_;}

  // get next item
  optional<T>next(){
    if(handle_.done())return std::nullopt;
    handle_();    // resume coroutine: handle_.resume <--> handle_()
    return handle_.done()?optional<T>{}:value();
  }
private:
  coroutine_handle<void>handle_; // keep handle so we can control coroutine (we don't know the promise_type - so void)
  T const&valref_;               // ref to value we store inside promise
};

// traits for looking up the promise object from a Gen<T> type
template <typename T>
struct std::coroutine_traits<Gen<T>,T>{
  // promise used when we need to store a single yield value
  class promise_type{
  public:
    Gen<T>get_return_object(){return Gen<T>(coroutine_handle<promise_type>::from_promise(*this),val_);}
    void return_void(){}
    std::suspend_always yield_value(T val){val_=val;return {};}
    std::suspend_always initial_suspend(){return {};}
    std::suspend_always final_suspend(){return {};}
    void unhandled_exception(){std::terminate();}
  private:
    T val_;
  };
};

// simple integer sequence
Gen<int>genint(int n){
  for(int i=0;i<n;++i)co_yield i;
}

// test program
int main(){
  auto gen=genint(5);
  while(auto val=gen.next())cout<<val.value()<<endl;
}

A quick conceptual snapshot

It's time to take birds eye snapshot view of what we have done so for. It's difficult to not get lost in the technicalities of the C++20 coroutine machinations.

The four main constructs we have dealt with are the promise_type, the future object, the coroutine itself and the coroutine_handle. The relationships between them are roughly (in our example):

Converted document

Before closing, it's worth mentioning that we could also have stored the value in the future object. Each time a new yield value was produced, we could have propagate it to the future so that a user could get hold of it. Of course, we would then have had to store a reference to the future inside the promise_type object.