Комментарии 11
Налито много воды. Вода мокрая. Таким образом, в статье мокро, потому что налито много мокрой воды. На всякий случай напишу ещё раз, что воды много, вдруг кто не понял.
Для кого вообще эта статья? Для кого расписаны все эти “Task<T> эквивалентен Task<T, StartSuspended>”, и почему банальныя фраза “значения по умолчанию для типов-параметров - void и StartSuspended” вообще нуждается в дальнейшей расшифровке?
Зачем тут в статье вообще очевидные детали реализации, вроде того что метод return_void написан потому что компилятор его требует?
Где примены использования вашего модуля? Где сравнение с сществующими реализациями (как минимум, с cppcoro)?
Да, замечание принимаю. Статья получилась слишком подробной в местах, где достаточно одной строки, особенно про default template arguments и очевидные элементы promise type. Статья ориентирована не на пользователей готовых coroutine-библиотек, а на тех, кто пишет собственный минимальный coroutine return object под свой scheduler/event loop и хочет явно контролировать promise_type, std::coroutine_handle, initial_suspend(), final_suspend() и уничтожение coroutine state.
Теперь претензии к коду. Ваш класс Task не поддерживает оператор co_await. Более того, он не предоставляет вообще никакой возможности дождаться окончания сопрограммы. Но при этом окончание сопрограммы требуется для вызова любого из его методов, включая деструктор!
Получается, этот код в принципе невозможно безопасно использовать. Нет, таких реализация нам не надо! Даже в качестве примера.
Да, первая часть замечания верная: текущий Task не является awaitable object и не предоставляет самостоятельного механизма ожидания завершения. Данный класс — не аналог cppcoro::task, а минимальный return object и RAII-владелец std::coroutine_handle.
Но утверждение, что для деструктора требуется именно окончание coroutine, неверно. Для coroutine_handle::destroy() требуется не final suspend, а suspended state. То есть coroutine может быть уничтожена и до завершения, например когда она находится в initial_suspend() или в другой точке приостановки. Завершение требуется для result(), но не для самого destroy().
Только вот уничтожение coroutine_handle в то время когда его позаимствовал чужой код, обрабатывающий co_await, приведёт к use after free. Уничтожать coroutine_handle в промежуточных точках можно только в тех случаях, когда все используемые awaitable могут корректно обработать такое уничтожение. А они не могут, потому что связать awaitable с промисом можно только через метод await_transform, которого у вас тоже нет.
Описываемый Вами сценарий не является режимом использования показанного Task<T, StartPolicy>.
В показанной реализации механизм удаления такой копии из внешних структур ожидания не задан. В моих проектах такой механизм выносится в scheduler/awaiter-слой; это архитектурное решение, и в другой реализации оно может быть организовано иначе. В этой статье этот слой не реализуется и не рассматривается.
По await_transform уточню отдельно: связь awaitable с promise не обязательно делается только через await_transform. await_suspend может принимать std::coroutine_handle<P> и через него обращаться к promise().
Для показанного Task<T, StartPolicy> сам по себе await_transform не нужен. Он потребуется только если нужно централизованно преобразовывать все co_await expr внутри coroutine body: например, привязывать awaitable к scheduler-у, добавлять cancellation state, execution context или tracing.
P.S. Добавил пояснение в подраздел «Почему выбран такой Task?».
Да, согласен, await_suspend может обращаться к promise напрямую.
Однако, в вашем promise всё равно нет и одного публичного метода, которые можно было бы использовать для предотвращения проблем.
В моих проектах такой механизм выносится в scheduler/awaiter-слой; это архитектурное решение, и в другой реализации оно может быть организовано иначе.
У вас в вашем Task недостаточно возможностей чтобы этот scheduler/awaiter-слой сделал хоть что-то.
В показанном Task действительно нет общего публичного интерфейса promise для cancellation/unregister-протокола, потому что такой протокол не входит в рассматриваемый уровень реализации.
При этом scheduler/awaiter-слой не обязан строиться только через публичные методы promise. Он может быть реализован через конкретные awaitable-типы: awaiter хранит ссылку на scheduler и registration id, а при уничтожении coroutine state его деструктор удаляет сохранённый handle из внешней структуры ожидания.
То есть такой механизм может находиться на уровне конкретного awaiter-а и scheduler-а, а не обязательно в самом Task или promise API.
Очередная gpt пародия на статью с таким же gpt генеративными ответами в комментариях... Когда нибудь такое будет подвергаться цензуре или запретам публикации?
Не, нейронки отвечают либо умнее, либо невпопад. Тут типичная протечка студенческой работы в интернет, когда студент выполнил семестровое задание и почему-то решил, что оно будет интересно кому-то кроме преподавателя. Ещё возможен вариант что это преподаватель тут пишет, в таком случае мне жаль его студентов.
Буду признателен за конкретные технические замечания: ошибки в коде, неверные формулировки по C++20 coroutines или несоответствия стандарту.
В текущем комментарии таких указаний нет, поэтому по существу статьи ответить не на что.

Универсальный Task для C++20 coroutines: тип результата, политика запуска и владение coroutine state