Сопрограммы в С++

1,00
р.
Недавно техническая спецификация сопрограмм, Coroutines TS, дошла до состояния "опубликована". Сопрограммы реализованы в MS VC++ 2017. Что это такое и как их писать?
(Upd: текущая версия Coroutines TS - n4775 от 2018-10-07) (Upd2: сопрограммы входят в С++20)

Ответ
В С++ сопрограммой называется функция, в которой используются co_await, co_yield, co_return.
Future coroutine(X x) { Y y = co_await f(x) co_return y }
Компилятор переписывает тело сопрограммы, превращая ее в машину состояний. Под данные сопрограммы выделяется память при помощи оператора new.
Future coroutine(X x) { struct CoroutineState { Future::promise_type p X x Y y int state = 0
void run() { switch (state) { case 0: ... state = 1 // приостановка return case 1: // точка возобновления ... } } } auto* s = new CoroutineState auto result = s->p.get_return_object() s->x = x s->run() return result }
При этом, хотя тело сопрограммы изменяется, она остается функцией.
promise_type (ч.1)
Компилятор добавляет неявную переменную с типом Future::promise_type. Эта переменная используется для создания результата функции-сопрограммы (p.get_return_object), обработки исключений, реализации co_return. Также компилятор добавляет точки приостановки в начале и конце сопрограммы, оборачивая тело сопрограммы в следующий код:
Future::promise_type p co_await p.initial_suspend() try { // тело сопрограммы Y y = co_await f(x) co_return y // конец тела сопрограммы } catch(...) { p.unhandled_exception() } final_suspend: co_await p.final_suspend()
co_return
В сопрограмме нельзя использовать return y , вместо него используется co_return y , который заменяется на
p.return_value(y) goto final_suspend
Если сопрограмма не предусматривает возврат значения по завершении, то используется co_return (без выражения) и соответствующая ему функция p.return_void() . При этом не обязательно писать co_return в конце сопрограммы.
co_await
Приостановка сопрограммы происходит в операторе co_await. Код Y y = co_await f(x) заменяется на
auto e = f(x) if (!e.await_ready()) { ... приостановка ... std::experimental::coroutine_handle<> h = ... if (e.await_suspend(h)) return resume: // точка возобновления для h.resume() ... возобновление ... } Y y = e.await_resume()
Стандартная библиотека предоставляет класс coroutine_handle, который позволяет возобновить приостановленную сопрограмму. Функция f принимает его через e.await_suspend(h). Когда значение y будет вычислено, она должна вызвать h.resume(), и вернуть вычисленное значение через e.await_resume().
Для stackless сопрограммы, функия f может быть написана следующим образом:
// Общие данные фонового потока и Awaiter. struct SharedState { std::experimental::coroutine_handle<> h Y value std::atomic is_ready }
// Тип результата f struct Awaiter { std::shared_ptr s
bool await_ready() { return false } bool await_suspend(std::experimental::coroutine_handle<> h) { s->h = h return !s->is_ready.exchange(true) // True если фоновый поток уже завершился // и у нас есть s->value } Y await_resume() { return s->value } }
Awaiter f(X x) { auto s = std::make_shared() std::thread([=]{ // Запуск фонового потока для вычислений s->value = ... if (s->is_ready.exchange(true)) { // True, если await_suspend уже была вызвана // и у нас есть s->h s->h.resume() } }).detach() return Awaiter{s} }
Для stackful сопрограммы (например Fibers в Windows), await_suspend должна сама замораживать поток (SwitchToFiber). Точка возобновления будет внутри await_suspend, поэтому она должна возвращать false.
promise_type (ч.2)
Минимальный тип возвращаемого значения сопрограммы выглядит так:
struct Future { struct promise_type { Future get_return_object() { return {this} } std::experimental::suspend_never initial_suspend() { return {} } std::experimental::suspend_always final_suspend() { return {} } void return_value(Y& y) { y_ptr = &y } std::atomic y_ptr = nullptr }
std::shared_ptr promise
static void Deleter(promise_type* p) { auto h = std::experimental::coroutine_handle

::from_promise(*p) h.destroy() // удаляет CoroutineState }
Future(promise_type* p) : promise(p, Deleter) {}
Y BlockingGet() { while (promise->y_ptr == nullptr) Sleep(1) // ждем return *promise->y_ptr // дождались вызова p.return_value(y) } }
Future::promise_type должен иметь get_return_object, initial_suspend, final_suspend и либо return_void либо return_value.
Для реализации initial_suspend и final_suspend можно использовать стандартные suspend_never и suspend_always, которые возвращают в await_ready значения true и false соответственно.
Такая сопрограмма будет всегда засыпать в конце.
От самого Future требуется только чтобы он удалил сопрограмму через h.destroy().
Future может (но не обязан) повторять интерфейс Awaiter, чтобы быть совместимым с co_await.
await_transform и operator co_await
Для выражения a в co_await a могут применяться дополнительные преобразования:
если выражение p.await_transform(a) валидно, то a заменяется на p.await_transform(a) если для типа a есть оператор operator co_await, то a заменяется на operator co_await(a) если в результате получилось prvalue, то оно копируется во временную переменную, иначе используется как есть.
Таким образом возможен вариант
auto& e = operator co_await(p.await_transform(f(x))) if (!e.await_ready()) { ... }
Например можно определить operator co_await(std::chrono::duration) и писать co_await 10ms .
Выделение памяти
Объект CoroutineState создается при помощи new. Однако, если есть функция p.get_return_object_on_allocation_failure(), то будет сгенерирован следующий код:
auto* s = new(std::nothrow) CoroutineState if (!s) { return p.get_return_object_on_allocation_failure() } auto result = s->p.get_return_object()
Это позволяет обрабатывать ошибки выделения памяти.
Также, аргументы сопрограммы могут участвовать в выделении памяти. Для сопрограммы Future coro(A1 a1, A2 a2), если есть функция operator new(std::size_t, A1, A2), то она будет вызвана вместо оператора new по умолчанию.
promise_type (ч.3), coroutine_traits
Компилятор использует класс coroutine_traits для получения promise_type.
Для сопрограммы Future coro(A1 a1, A2 a2) будет использован тип std::experimental::coroutine_traits::promise_type.
Реализация по-умолчанию выдает Future::promise_type, однако это может быть переопределено пользователем.
co_yield
co_yield e эквивалентен co_await p.yield_value(e) и используется в генераторах - специальных сопрограммах которые предназначены для выдачи последовательности значений.
Примеры
Как использовать сопрограммы С++ с Boost.Asio?