With Boost.Coroutine it is possible to use coroutines in C++. Coroutines are a feature of other programming languages, which often use the keyword yield
for coroutines. In these programming languages, yield
can be used like return
. However, when yield
is used, the function remembers the location, and if the function is called again, execution continues from that location.
C++ doesn’t define a keyword yield
. However, with Boost.Coroutine it is possible to return from functions and continue later from the same location. The Boost.Asio library also uses Boost.Coroutine and benefits from coroutines.
There are two versions of Boost.Coroutine. This chapter introduces the second version, which is the current version. This version has been available since Boost 1.55.0 and replaces the first one.
#include <boost/coroutine/all.hpp>
#include <iostream>
using namespace boost::coroutines;
void cooperative(coroutine<void>::push_type &sink)
{
std::cout << "Hello";
sink();
std::cout << "world";
}
int main()
{
coroutine<void>::pull_type source{cooperative};
std::cout << ", ";
source();
std::cout << "!\n";
}
Example 51.1 defines a function, cooperative()
, which is called from main()
as a coroutine. cooperative()
returns to main()
early and is called a second time. On the second call, it continues from where it left off.
To use cooperative()
as a coroutine, the types pull_type
and push_type
are used. These types are provided by boost::coroutines::coroutine
, which is a template that is instantiated with void
in Example 51.1.
To use coroutines, you need pull_type
and push_type
. One of these types will be used to create an object that will be initialized with the function you want to use as a coroutine. The other type will be the first parameter of the coroutine function.
Example 51.1 creates an object named source of type pull_type
in main()
. cooperative()
is passed to the constructor. push_type
is used as the sole parameter in the signature of cooperative()
.
When source is created, the function cooperative()
, which is passed to the constructor, is immediately called as a coroutine. This happens because source is based on pull_type
. If source was based on push_type
, the constructor wouldn’t call cooperative()
as a coroutine.
cooperative()
writes Hello
to standard output. Afterwards, the function accesses sink as if it were a function. This is possible because push_type
overloads operator()
. While source in main()
represents the coroutine cooperative()
, sink in cooperative()
represents the function main()
. Calling sink makes cooperative()
return, and main()
continues from where cooperative()
was called and writes a comma to standard output.
Then, main()
calls source as if it were a function. Again, this is possible because of the overloaded operator()
. This time, cooperative()
continues from the point where it left off and writes world
to standard output. Because there is no other code in cooperative()
, the coroutine ends. It returns to main()
, which writes an exclamation mark to standard output.
The result is that Example 51.1 displays Hello, world!
You can think of coroutines as cooperative threads. To a certain extent, the functions main()
and cooperative()
run concurrently. Code is executed in turns in main()
and cooperative()
. Instructions inside each function are executed sequentially. Thanks to coroutines, a function doesn’t need to return before another function can be executed.
#include <boost/coroutine/all.hpp>
#include <functional>
#include <iostream>
using boost::coroutines::coroutine;
void cooperative(coroutine<int>::push_type &sink, int i)
{
int j = i;
sink(++j);
sink(++j);
std::cout << "end\n";
}
int main()
{
using std::placeholders::_1;
coroutine<int>::pull_type source{std::bind(cooperative, _1, 0)};
std::cout << source.get() << '\n';
source();
std::cout << source.get() << '\n';
source();
}
Example 51.2 is similar to the previous example. This time the template boost::coroutines::coroutine
is instantiated with int
. This makes it possible to return an int
from the coroutine to the caller.
The direction the int
value is passed depends on where pull_type
and push_type
are used. The example uses pull_type
to instantiate an object in main()
. cooperative()
has access to an object of type push_type
. push_type
sends a value, and pull_type
receives a value; thus, the direction of the data transfer is set.
cooperative()
calls sink, with a parameter of type int
. This parameter is required because the coroutine was instantiated with the data type int
. The value passed to sink is received from source in main()
by using the member function get()
, which is provided by pull_type
.
Example 51.2 also illustrates how a function with multiple parameters can be used as a coroutine. cooperative()
has an additional parameter of type int
, which can’t be passed directly to the constructor of pull_type
. The example uses std::bind()
to link the function with pull_type
.
The example writes 1
and 2
followed by end
to standard output.
#include <boost/coroutine/all.hpp>
#include <tuple>
#include <string>
#include <iostream>
using boost::coroutines::coroutine;
void cooperative(coroutine<std::tuple<int, std::string>>::pull_type &source)
{
auto args = source.get();
std::cout << std::get<0>(args) << " " << std::get<1>(args) << '\n';
source();
args = source.get();
std::cout << std::get<0>(args) << " " << std::get<1>(args) << '\n';
}
int main()
{
coroutine<std::tuple<int, std::string>>::push_type sink{cooperative};
sink(std::make_tuple(0, "aaa"));
sink(std::make_tuple(1, "bbb"));
std::cout << "end\n";
}
Example 51.3 uses push_type
in main()
and pull_type
in cooperative()
, which means data is transferred from the caller to the coroutine.
This example illustrates how multiple values can be passed. Boost.Coroutine doesn’t support passing multiple values, so a tuple must be used. You need to pack multiple values into a tuple or another structure.
Example 51.3 displays 0 aaa
, 1 bbb
, and end
.
#include <boost/coroutine/all.hpp>
#include <stdexcept>
#include <iostream>
using boost::coroutines::coroutine;
void cooperative(coroutine<void>::push_type &sink)
{
sink();
throw std::runtime_error("error");
}
int main()
{
coroutine<void>::pull_type source{cooperative};
try
{
source();
}
catch (const std::runtime_error &e)
{
std::cerr << e.what() << '\n';
}
}
A coroutine returns immediately when an exception is thrown. The exception is transported to the caller of the coroutine where it can be caught. Thus, exceptions are no different than with regular function calls.
Example 51.4 shows how this works. This example will write the string error
to standard output.