Информация об изменениях

Сообщение Re: C++20 coroutines (co_await) от 09.01.2021 5:22

Изменено 09.01.2021 6:49 х

Re: C++20 coroutines (co_await)
Здравствуйте, okman, Вы писали:

O>Всем привет!


O>Изучаю C++20. В целом все понятно, но одна вещь вызывает наибольшие затруднения — корутины.


Всё просто, если немного упростить с деталями:
реализуешь шаблон coroutine_traits с внутренней структурой promise_type — это объект который будет использоваться в функции использующей корутины для управления работой.

По сути функция
T f(T1 a1, T2 a2 ...) {

   co_await ...
   co_return ...
}


преобразуется в:
T f(T1 a1, T2 a2 ...) {
struct Arguments {
  coroutine_traits<T, T1, T2...>::promise_type this_corutine;
  T1 a1;
  T2 a2;
};
  Arguments args { coroutine_traits::promise_type, std::move(a1), std::move(a2) ... };
  ...
  return args->this_corutine.get_return_object();
}


То есть при первом же вызове оператора корутин формируется структура для работы с ними.


Далее когда есть вызов:
auto res = co_await fnested(...);


и пусть при этом
TResult fnested(...)


тогда co_await преобразуется в (понятно, что move расставлены не корректно, но они для демонстрации передачи владением объекта в другой код)
  auto fnested_awaiter = operator co_await(fnested(...));

  auto rest_code = [std::move(fnested_awaiter), std::move(args)]() {
      auto res = fnested_awaiter.await_resume();
      ...//Rest code
  };

  if(!fnested_awaiter.await_ready()){
    //Suspend
    fnested_awaiter.await_suspend(std::move(rest_code));
  } else {
    rest_code();
  }


То есть вызывается вложенная функция и у результата этой функции спрашивается готов ли результат. Если он готов, то результат забирается через await_resume иначе просто остаток функции упаковывается в лямбду и через await_suspend говорится, что когда вложенная функция отработает надо вызвать нашу лямбду для продолжения выполнения нашей функции.

co_return ещё проще — он просто вызывает метод return_value для вашего promise_type.

Таким образом
T f(T1 a1, T2 a2 ...) {

   auto res = co_await fnested(...);
   co_return ...;
}


преобразуется в что-то вида:
T f(T1 a1, T2 a2 ...) {
 struct Arguments {
  coroutine_traits<T, T1, T2...>::promise_type this_corutine;
  T1 a1;
  T2 a2;
 };
 Arguments args { coroutine_traits::promise_type, std::move(a1), std::move(a2) ... };
 
 auto fnested_awaiter = operator co_await(fnested(...));
 auto rest_code = [std::move(fnested_awaiter), args]() {
      auto res = fnested_awaiter.await_resume();
      args->this_corutine.return_value(...);//co_return ...;
  };
 if(!fnested_awaiter.await_ready()){
  //Suspend
  fnested_awaiter.await_suspend(std::move(rest_code));
 } else {
  rest_code();
 }
 return args->this_corutine.get_return_object();
}


Собственно всё. Дальше Вы сами решаете как строить цепочки функций, как производить "просыпание" и т.п. В этом мощь. В отличии от C# вы тут можете выбирать ту модель которую считаете оптимальной.

Более детально можно посмотреть реализацию тут.
Re: C++20 coroutines (co_await)
Здравствуйте, okman, Вы писали:

O>Всем привет!


O>Изучаю C++20. В целом все понятно, но одна вещь вызывает наибольшие затруднения — корутины.


Всё просто, если немного упростить с деталями:
реализуешь шаблон coroutine_traits с внутренней структурой promise_type — это объект который будет использоваться в функции использующей корутины для управления работой.

По сути функция
T f(T1 a1, T2 a2 ...) {

   co_await ...
   co_return ...
}


преобразуется в:
T f(T1 a1, T2 a2 ...) {
struct Arguments {
  coroutine_traits<T, T1, T2...>::promise_type this_corutine;
  T1 a1;
  T2 a2;
};
  Arguments args { coroutine_traits::promise_type, std::move(a1), std::move(a2) ... };
  ...
  return args->this_corutine.get_return_object();
}


То есть при первом же вызове оператора корутин формируется структура для работы с ними.


Далее когда есть вызов:
auto res = co_await fnested(...);


и пусть при этом
TResult fnested(...)


тогда co_await преобразуется в (понятно, что move расставлены не корректно, но они для демонстрации передачи владением объекта в другой код)
  auto fnested_awaiter = operator co_await(fnested(...));

  auto rest_code = [std::move(fnested_awaiter), std::move(args)]() {
      auto res = fnested_awaiter.await_resume();
      ...//Rest code
  };

  if(!fnested_awaiter.await_ready()){
    //Suspend
    fnested_awaiter.await_suspend(std::move(rest_code));
  } else {
    rest_code();
  }


То есть вызывается вложенная функция и у результата этой функции спрашивается готов ли результат. Если он готов, то результат забирается через await_resume иначе просто остаток функции упаковывается в лямбду и через await_suspend говорится, что когда вложенная функция отработает надо вызвать нашу лямбду для продолжения выполнения нашей функции.

co_return ещё проще — он просто вызывает метод return_value для вашего promise_type.

Таким образом
T f(T1 a1, T2 a2 ...) {

   auto res = co_await fnested(...);
   co_return ...;
}


преобразуется в что-то вида:
T f(T1 a1, T2 a2 ...) {
 struct Arguments {
  coroutine_traits<T, T1, T2...>::promise_type this_corutine;
  T1 a1;
  T2 a2;
 };
 Arguments args { coroutine_traits::promise_type, std::move(a1), std::move(a2) ... };
 
 auto fnested_awaiter = operator co_await(fnested(...));
 auto rest_code = [std::move(fnested_awaiter), std::move(args)]() {
      auto res = fnested_awaiter.await_resume();
      args->this_corutine.return_value(...);//co_return ...;
  };
 if(!fnested_awaiter.await_ready()){
  //Suspend
  fnested_awaiter.await_suspend(std::move(rest_code));
 } else {
  rest_code();
 }
 return args->this_corutine.get_return_object();
}


Собственно всё. Дальше Вы сами решаете как строить цепочки функций, как производить "просыпание" и т.п. В этом мощь. В отличии от C# вы тут можете выбирать ту модель которую считаете оптимальной.

Более детально можно посмотреть реализацию тут.