суббота, 31 октября 2026 г.

[prog.c++] Пара слов о stackless короутинах в C++20

По мере продвижения работ по встраиванию C++20 короутин в SObjectizer накапливаю опыт работы с этими самыми короутинами. Пока его еще мало, чтобы составить какой-то адекватный список проблем. У меня вообще нет окончательного мнения о том, кто был автором короутин в C++20 -- гении, безумцы, или гениальные безумцы, или все вместе, или одно из двух. Ну да ладно, пока что озвучу пару вещей, которая меня временами то ли напрягает, то ли вымораживает.


Основной момент -- это конструирование экземпляра promise_type при создании короутины. У promise_type вызывается либо дефолтный конструктор (без параметров), либо же конструктор, куда можно запихнуть все аргументы функции, являющейся короутиной. Т.е. если у нас есть:

task_t my_coro(int a, char * b) {...}

то у task_t::promise_type должен быть либо дефолтный конструктор, либо конструктор вида:

promise_type(int, char *)

ну или похожий на этот конструктор с учетом возможности неявного преобразования аргументов (это же C++, здесь просто быть не может).

Однако, ничего другого в этот самый конструктор вы предать просто так не можете. Например, захотели вы, чтобы именно этот конкретный экземпляр my_coro был связан с конкретным аллокатором... Ну захотели и захотели. Перехотите.

Или вот мне захотелось уметь привязывать task_t к конкретному планировщику, а в прототипе my_coro нет никаких планировщиков -- только int и char*. И все, выдумывай что хочешь.

Что тут можно было бы придумать?

А вот пока не знаю. Для меня вообще stackless короутины -- это странный и непривычный зверь с очень ограниченной областью полезного применения. Пока еще не набил с ними достаточного количества шишек. Поэтому могу только фантазировать.

Например, почему бы не сделать новый оператор co_create для создания экземпляра короутины? В который можно было бы запихивать разные параметры. Да, тогда код мог бы выглядеть слишком уж co_страшновато:

auto r = co_await co_create(allocator, scheduler) socket.connect(ip_address);

но, может быть, для совсем простых, вырожденных случаев, когда дополнительные параметры для короутины передавать не нужно, можно было бы вообще без co_create обойтись.

Может быть в co_create можно было бы предавать и признак того как именно короутина должна стартовать -- энергично или лениво:

auto c = co_create(std::lazy) socket.connect(ip_address);
...
auto r = co_await c;

Но на счет этого не уверен совершенно.


Еще одна штука масштабом сильно помельче, но гораздо более раздражающая.

Метод await_suspend у Awaiter-а может иметь разные типы возвращаемых значений:

  • void. Приостанавливать текущую короутину нужно обязательно;
  • std::coroutine_handle. Нужно возобновить работу указанной короутины. В том числе это может быть и текущая короутина если приостанавливать ее не пришлось;
  • bool. Нужно или не нужно приостанавливать текущую короутину определяется возвращенным значением.

И вот с bool-ом лично у меня постоянные заминки. Ну не могу я запомнить что означает true: приостановить короутину или же нет.

И тут спрашивается: а что мешало вместо bool сделать специальный тип, вроде:

enum class await_suspend_result { suspend, resume };

Т.е. если await_suspend возвращает await_suspend_result::suspend, значит текущую короутину нужно таки заморозить. А если await_suspend_result::resume, то наоборот.

Ну ведь просто же и понятно. Так почему нет? Экономия на новых типах в стандартной библиотеке?