C++20 Coroutines back to basics — Restrictions and rules for legal coroutines

1. Introduction

In my previous articles on coroutines, I attempted to explain the mechanics of coroutine suspension, resuming it, promise_type, awaiters and awaitables. In this article, I would like to answer two very basic questions:

  1. What are the restrictions on coroutines ?
  2. When can a coroutine usage be legal in terms of member, non-member functions of class/structs, lambda expressions and so on ?

1.1 What this post is not about

While answering the above two questions, I will not dive into how the coroutines work or explain the compiler magic that happens when a coroutine is in execution. Nor will I explain about the lifetime of coroutines’ activation frame or the parameters passed to coroutines.

So without much ado lets jump right in.

2. Restrictions on C++20 Coroutines

Cpp reference page on coroutines says the following about restrictions on coroutines:

Coroutines cannot use variadic arguments, plain return statements, or placeholder return types (auto or Concept). Constexpr functions, constructors, destructors, and the main function cannot be coroutines.

This statement is quite straight forward and packs all relevant information. But I would like to add some details to it and probably clarify a few points and expan on what could be legal and illgal for a coroutine

2.1 coroutines and varidaic arguments : ILLEGAL

Variadic arguments in a function appears with three dots, or more technically an ellipsis ( ... ) . For e.g consider the function foo below:

void foo(...); //<-- variadic arguments

Now according to the standard, if a coroutine has varidic arguments, it is illegal. Therefore assuming a valid ReturnObject for a coroutine, the following coroutine is illegal :

ReturnObject coro_with_variadic_args(...) //<-- variadic arguments
{
//....
}

An attempt to compile such a coroutine will not succeed and compilers adhere to the standards. However gcc-11.3 doesn’t give a intuitive error message but Clang-14 and MSVC-19 complain rightly about illegal usage of variadics with coroutines. You can check the code and error messages on compiler explorer

2.2 coroutines and parameter pack : LEGAL

A parameter pack in a function template looks like below

template<typename ...Args>
void foo(Args ...args); //<-- Parmeter Pack

With C++20’s abbreviated function template syntax (sorry for the shameless plug to my own article) the above function can be re-written as :

void foo(auto ...args); //<-- Parmeter Pack

Surprisingly a coroutine with parameter pack is legal, i.e., a coroutine with parameter pack below

ReturnObject coro_w_parameter_pack(auto ...args) //<- parameter pack
{
//....
}

compiles and runs fine as long as it doesn’t violate life time rules of the parameters and its activation frame. You can check a working example for coroutine with parameter pack on compiler explorer. In my opinion the standard could have clarified this better on why variadic arguments are illegal and why parameter packs are legal.

2.3 Coroutines with placeholder return types: ILLEGAL

Simply re-stated : A coroutine cannot have auto or decltype(auto) as its return type since the compiler needs to know the promise_type object associated with coroutine before it executes. Therefore a simple coroutine with just a co_return statement in its body

/*Note auto here */ auto coro_w_auto_return()
{ co_return; }

will not compile and the compiler rightly complains about coroutine being used with deduced return type. You can check the code and compiler errors on compiler explorer.

2.3 Coroutines with trailing return types: LEGAL

By simply adding the correct return type as a trailing return type in the coroutine declared with auto in its return statement the coroutine becomes legal. There’s no magic here. So I encourage the readers to try it out on the previous code on compiler explorer

The remaining restrictions on constexpr , consteval with coroutines and main function, constructors and destructors being used as coroutines have not gotchas so I will not discuss them further.

3. Coroutines and usage as different functions

In my previous articles on coroutines, I only demonstrated the mechanics of coroutines with the help of free standing functions. This was intentional to keep the focus on relevant detail and keep the code direct instead of getting tangled in class hierarchies, complex temporary callbacks with life time issues and other trickries of C++. However coroutines can be used not only as free functions, but also like other functions for e.g the following usage of coroutines are completely legal

3.1 Coroutines as member function of class

Like its non-coroutine cousins, coroutines can be used as member functions. In fact they can be used as virtual functions and be overriden by classes down in the hierarchy . A compiler will not be able to establish if a pure virtual function with a ReturnObject is a coroutine or a normal function. This can allow a derived class to call a coroutine in a overidden function. For e.g. consider the code below. The Derived_2::virtual_coro is a normal function that calls the Derived_1::virtual_coro in its body

struct Base{
virtual ReturnObject virtual_coro() = 0;
};
struct Derived_1 : Base{
ReturnObject virtual_coro() override
{
co_await std::suspend_always{};
co_yield 10;
}
struct Derived_2: Base{
ReturnObject virtual_coro() override
{
//calling another coroutine, here Derived1::virtual_coro
//virtual_coro therefore acts as a normal function
return Derived_1::virtual_coro();
}
};

Thus as seen from example above, we can override a virtual function to act a coroutine or act as a normal function that can execute a coroutine. Pretty savvy!

3.2 Coroutines as lambda expression

Yes! That’s right. C++ gives us the flexibility to combine coroutines and labmda expressions as long as it satisfies all the requirements one of which being the explicit specification of the ReturnObject with underlying promise_type in the trailing return type syntax in lambda expression which is a coroutine. For e.g the simplest coroutine declared as lambda expression that captures nothing, accepts no arguments and simply returns looks like below:

auto lambda_coro = [] -> ReturnObject { co_return;}

This gives user to launch cooperatively asynchronous task with STL containers provided one doesn’t run into complex lifetime issues. To me coroutine as lambda expression seems a powerful feature that needs to be used responsibly (shameless quote from spider man!).

3.3 Coroutine as static (member) function

Coroutines can be used as free standing static function or a static member function of a class. For e.g the code snippet below having a static member coroutine function and a static free coroutine function

struct Foo{      //static member coroutine
static
ReturnObject static_member_coro()
{
co_return;
}
};
//free static standing coroutine
static
ReturnObject static_free_standing_coro()
{
co_return;
}

are completely valid and legal.

4. Conclusion

Coroutines are quite flxible in terms of how they can be used as long as they satisfy the restrictions and requirements. However managing life time of coroutines’ activation frame and the parameters that are passed to coroutines is still a complex topic that I will try to deal with in my next issue. So stay tuned for more modern c++.

--

--

--

I'm a backend software developer and from time to time I also like to explore web development

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Introducing Pinwheel, the API for Payroll

10 Interesting Things About HTML5 And CSS3

Things to know before choosing a bootcamp

My lesson learn in Solana Smart Contract development

C#: Switch statements and why you should use them

Shadow IT Solutions — 3 Things You Need To Know About Shadow IT

How to deploy your HTML emails with zero cost

How to convert Text to Binary format using an online tool?

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Gajendra Gulgulia

Gajendra Gulgulia

I'm a backend software developer and from time to time I also like to explore web development

More from Medium

Heap and heap: The 114 C++ algorithms series

C++ 20 concurrency Part 1: synchronized output stream

What “volatile” does in C (and C++)

C — Static libraries