В статье рассматривается заголовочный компонент execution_core::Task, предназначенный для использования как return object coroutine-функций C++20.

Coroutine-функция в C++20 — это функция, в теле которой используется co_await, co_yield или co_return [2]. Для coroutine-функции promise type определяется через возвращаемый тип функции и std::coroutine_traits. [1]

Рассматриваемая реализация:

  • задаёт promise_type для Task<void> и Task<T> при T != void;

  • задаёт две политики начальной приостановки: StartSuspended и StartImmediately;

  • хранит std::coroutine_handle<promise_type>;

  • уничтожает coroutine state через coroutine_handle::destroy();

  • сохраняет исключение в std::exception_ptr;

  • для Task<T> при T != void сохраняет результат в std::optional<T>.

Термин «модуль» далее используется в архитектурном смысле. Код является header-only компонентом. Это не C++20 module unit, так как в нём используется #pragma once, а не export module.

Почему выбран такой Task?

Цель этой реализации - показать универсальный шаблон Task<T, StartPolicy>, в котором выбор поведения задаётся параметрами шаблона, а не набором отдельных несвязанных классов.

Важно: вопросы co_await Task<T>, ожидания завершения, cancellation/unregister-протокола и scheduler-а относятся к другому уровню реализации и в этот шаблон намеренно не включены.

Параметр T выбирает форму promise object: для T == void используется void_promise, для T != void используется value_promise<T>.

Параметр StartPolicy выбирает поведение initial_suspend(): StartSuspended даёт std::suspend_always, а StartImmediately даёт std::suspend_never.

Таким образом, одна шаблонная форма Task<T, StartPolicy> задаёт return object для нескольких случаев использования coroutine-функций без дублирования основной логики владения std::coroutine_handle.

C++20 coroutines задают языковой механизм приостановки и возобновления coroutine body, но не задают готовый универсальный тип результата для пользовательской coroutine-функции. Coroutine-функция должна иметь return type. Для этого return type компилятор через std::coroutine_traits определяет promise_type [2].

Следовательно, пользовательский тип Task<T, StartPolicy> нужен как тип результата coroutine-функции, через который задаются:

  • тип promise object;

  • объект, возвращаемый из coroutine-функции;

  • способ получить std::coroutine_handle;

  • способ управлять lifetime coroutine state;

  • место хранения результата или исключения;

  • начальное состояние coroutine body после вызова coroutine-функции.

Без такого return object coroutine-функция не получает пользовательского объекта управления. Языковой механизм создаёт coroutine state и promise object, но пользовательскому коду нужен объект, через который этот state будет доступен и уничтожен.

В данной реализации таким объектом является Task<T, StartPolicy>. Он связывает coroutine-функцию, promise object и coroutine handle в один объект владения.

В таком построении нет претензии на оригинальность: этот вариант наверняка не является уникальным и мог быть независимо реализован другими разработчиками. Здесь он рассматривается как один из возможных минимальных вариантов пользовательского return object для C++20 coroutine-функций. Материал может быть полезен тем, кто хочет явно увидеть, как связаны promise_type, std::coroutine_handle, initial_suspend(), final_suspend() и lifetime coroutine state.

Исходный код

Исходный код Task
#pragma once

#include <coroutine>
#include <exception>
#include <optional>
#include <type_traits>
#include <utility>
#include <cassert>

namespace execution_core {

struct StartImmediately {};
struct StartSuspended {};

template<typename T = void, typename StartPolicy = StartSuspended>
class Task {
    static_assert(
        std::is_same_v<StartPolicy, StartImmediately> ||
        std::is_same_v<StartPolicy, StartSuspended>
    );

private:
    template<typename Promise>
    struct base_promise {
        std::exception_ptr exception;

        Task get_return_object() noexcept {
            return Task{
                std::coroutine_handle<Promise>::from_promise(
                    static_cast<Promise&>(*this)
                )
            };
        }

        auto initial_suspend() noexcept {
            if constexpr (std::is_same_v<StartPolicy, StartImmediately>) {
                return std::suspend_never{};
            } else {
                return std::suspend_always{};
            }
        }

        std::suspend_always final_suspend() noexcept {
            return {};
        }

        void unhandled_exception() {
            exception = std::current_exception();
        }
    };

    struct void_promise final : base_promise<void_promise> {
        void return_void() noexcept {}
    };

    template<typename U>
    struct value_promise final : base_promise<value_promise<U>> {
        std::optional<U> result;

        template<typename V>
        void return_value(V&& value) {
            result.emplace(std::forward<V>(value));
        }
    };

public:
    using promise_type = std::conditional_t<
        std::is_void_v<T>,
        void_promise,
        value_promise<T>
    >;

    using handle_type = std::coroutine_handle<promise_type>;

public:
    Task() noexcept = default;

    explicit Task(handle_type handle) noexcept
        : handle_(handle)
    {}

    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;

    Task(Task&& other) noexcept
        : handle_(std::exchange(other.handle_, {}))
    {}

    Task& operator=(Task&& other) noexcept {
        if (this != &other) {
            destroy();
            handle_ = std::exchange(other.handle_, {});
        }
        return *this;
    }

    ~Task() {
        destroy();
    }

    void start() {
        if constexpr (std::is_same_v<StartPolicy, StartSuspended>) {
            if (handle_ && !handle_.done()) {
                handle_.resume();
            }
        }
    }

    bool done() const noexcept {
        return !handle_ || handle_.done();
    }

    void rethrow_if_exception() {
        if (handle_ && handle_.promise().exception) {
            std::rethrow_exception(handle_.promise().exception);
        }
    }

    handle_type native_handle() const noexcept {
        return handle_;
    }

    explicit operator bool() const noexcept {
        return static_cast<bool>(handle_);
    }

    template<typename U = T>
    requires (!std::is_void_v<U>)
    U& result() & {
        assert(handle_);
        assert(handle_.done());

        rethrow_if_exception();

        assert(handle_.promise().result.has_value());
        return *handle_.promise().result;
    }

    template<typename U = T>
    requires (!std::is_void_v<U>)
    const U& result() const& {
        assert(handle_);
        assert(handle_.done());

        if (handle_.promise().exception) {
            std::rethrow_exception(handle_.promise().exception);
        }

        assert(handle_.promise().result.has_value());
        return *handle_.promise().result;
    }

    template<typename U = T>
    requires (!std::is_void_v<U>)
    U&& result() && {
        assert(handle_);
        assert(handle_.done());

        rethrow_if_exception();

        assert(handle_.promise().result.has_value());
        return std::move(*handle_.promise().result);
    }

private:
    void destroy() noexcept {
        if (handle_) {
            handle_.destroy();
            handle_ = {};
        }
    }

private:
    handle_type handle_{};
};

} // namespace execution_core

Область ответственности Task

Task<T, StartPolicy> задаёт return object coroutine-функции.

Return object coroutine-функции создаётся через вызов promise.get_return_object(). Этот вызов предшествует вызову promise.initial_suspend() и выполняется не более одного раза [1].

В данной реализации Task не является scheduler, event loop или thread pool. Он не выбирает поток выполнения и не содержит очереди готовых coroutine handle.

Область ответственности Task состоит из следующих элементов:

  • определение promise_type;

  • получение std::coroutine_handle<promise_type> из promise object;

  • хранение coroutine handle;

  • уничтожение coroutine state;

  • доступ к результату для Task<T> при T != void;

  • хранение необработанного исключения через promise object.

Типы политики запуска

В коде определены два пустых типа: StartImmediately и StartSuspended. Они используются как значения параметра шаблона StartPolicy.

Ограничение допустимых типов задано через static_assert: StartPolicy должен быть либо StartImmediately, либо StartSuspended.

Тип по умолчанию — StartSuspended. Следовательно, Task<T> эквивалентен Task<T, StartSuspended>, а Task<> эквивалентен Task<void, StartSuspended>.

Поддерживаемые формы return type coroutine-функций:

  • для void-результата: Task<>, Task<void>, Task<void, StartSuspended>, Task<void, StartImmediately>;

  • для результата-значения при T != void: Task<T>, Task<T, StartSuspended>, Task<T, StartImmediately>.

Почему разделены void- и value-варианты promise type

Обработка co_return определяется не самим типом Task, а правилами coroutine promise.

Для co_return; или co_return с operand типа void используется p.return_void().

Для co_return expr, где operand является braced-init-list или expression non-void type, используется p.return_value(expr-or-braced-init-list).

При этом если в scope promise type одновременно найдены имена return_void и return_value, программа является ill-formed [1].

Поэтому один общий promise type с обоими методами не соответствует этому ограничению.

В данной реализации выбор выполняется на этапе компиляции:

using promise_type = std::conditional_t<
    std::is_void_v<T>,
    void_promise,
    value_promise<T>
>;

Следствие:

Task<void>              -> void_promise
Task<T>, T != void      -> value_promise<T>

То есть для Task<void> существует promise type с return_void(), а для Task<T> при T != void существует promise type с return_value(...).

base_promise

void_promise и value_promise<T> различаются способом обработки co_return.

Общими для них остаются get_return_object(), initial_suspend(), final_suspend(), unhandled_exception() и поле exception.

Поэтому общая часть вынесена в base_promise<Promise>. Это устраняет дублирование одинаковых функций promise object, но сохраняет разные фактические promise types: void_promise и value_promise<T>.

base_promise<Promise> использует фактический тип promise через параметр Promise, потому что std::coroutine_handle<Promise>::from_promise(...) должен получить ссылку именно на фактический promise object [1][3].

В get_return_object() выполняется преобразование static_cast<Promise&>(*this), после чего создаётся coroutine handle через std::coroutine_handle<Promise>::from_promise(...).

Для from_promise стандарт задаёт постусловие addressof(h.promise()) == addressof(p), где p — promise object, из которого создан handle [1].

Последовательность создания coroutine return object

Для coroutine-функции с return type Task<T> при T != void используется Task<T>::promise_type, то есть value_promise<T>.

Для coroutine-функции с return type Task<void> используется Task<void>::promise_type, то есть void_promise.

Последовательность на уровне модели C++20:

  1. вызывается coroutine-функция;

  2. создаётся coroutine state;

  3. в coroutine state создаётся promise object;

  4. вызывается promise.get_return_object();

  5. get_return_object() создаёт Task;

  6. Task получает std::coroutine_handle<promise_type>;

  7. вызывается promise.initial_suspend();

  8. дальнейшее поведение зависит от результата initial_suspend().

Эта последовательность соответствует модели coroutine body, где после получения return object выполняется co_await promise.initial_suspend() [1][2].

StartPolicy

Начальное поведение coroutine body определяется результатом promise.initial_suspend().

В этой реализации рассматривается два режима:

  • StartSuspended задаёт initial_suspend() -> std::suspend_always;

  • StartImmediately задаёт initial_suspend() -> std::suspend_never.

std::suspend_always задаёт awaitable object, у которого await_ready() возвращает false [4]. Следовательно, для Task<T, StartSuspended> coroutine body после вызова coroutine-функции остаётся в начальной точке приостановки. Запуск выполняется явно через task.start().

Такой режим нужен, когда coroutine handle должен быть сначала получен, сохранён во внешней структуре управления или передан scheduler-у, и только после этого coroutine body должна начать выполнение.

std::suspend_never задаёт awaitable object, у которого await_ready() возвращает true [5]. Следовательно, для Task<T, StartImmediately> coroutine body не останавливается в начальной точке приостановки и начинает выполнение сразу после создания return object.

StartPolicy в этой реализации задаёт не runtime-флаг, а compile-time выбор результата initial_suspend().

start()

Функция start() имеет действие только для Task<T, StartSuspended>.

Для Task<T, StartImmediately> после подстановки if constexpr тело функции не содержит вызова handle_.resume().

Для StartSuspended выполняются проверки handle_ и !handle_.done(), после чего вызывается handle_.resume().

coroutine_handle::resume() возобновляет выполнение coroutine, на которую ссылается coroutine handle [1][3].

Важно: coroutine_handle::done() имеет precondition: handle должен ссылаться на suspended coroutine. Task::done() и проверка !handle_.done() в start() корректны только при условии, что coroutine в данный момент не выполняется, а находится в suspended state.

resume() допустим только для handle, который ссылается на suspended coroutine, причём coroutine не должна находиться в final suspend point.

Роль std::suspend_always в final_suspend() этой реализации

При завершении coroutine body выполняется co_await promise.final_suspend() [1][2].

В данной реализации final_suspend() всегда возвращает std::suspend_always.

После выполнения co_return результат сохраняется внутри promise object в handle_.promise().result. Исключение сохраняется внутри promise object в handle_.promise().exception.

Метод result() читает эти данные после завершения coroutine body. Поэтому coroutine state должен существовать после завершения coroutine body до момента, когда Task вызовет handle_.destroy().

Если coroutine state был бы уничтожен до вызова result(), доступ к promise object через handle_.promise() был бы невозможен.

Сохранение результата после co_return

Для Task<T> при T != void используется value_promise<T>, внутри которого хранится std::optional<T> result.

std::optional<T> представляет объект, который либо содержит значение типа T, либо не содержит значения [7].

До выполнения co_return value объект result не содержит значения. При выполнении co_return value вызывается promise.return_value(value). В данной реализации это приводит к вызову result.emplace(std::forward<V>(value)).

Следовательно, std::optional<T> используется как storage для результата, который появляется не при создании coroutine state, а при выполнении co_return.

Обработка void-результата

Для void используется void_promise, содержащий return_void().

Для coroutine-функции с return type Task<void> или Task<> при выполнении co_return; используется return_void() [1][2].

Результат-значение в этом случае не хранится.

Доступ к результату

Методы result() существуют только для Task<T> при T != void.

В реализации это задано через constraint:

template<typename U = T>
requires (!std::is_void_v<U>)

Следовательно, для Task<void> методы result() не участвуют в overload resolution, а для Task<T> при T != void доступны три overload:

U&       result() &;
const U& result() const&;
U&&      result() &&;

Они различаются ref-qualifier-ом функции-члена:

  • для lvalue-объекта Task<T> task выбирается U& result() &;

  • для const lvalue-объекта const Task<T> task выбирается const U& result() const&;

  • для rvalue-объекта std::move(task).result() выбирается U&& result() &&.

Условия корректного вызова result() в данной реализации выражены проверками:

assert(handle_);
assert(handle_.done());
assert(handle_.promise().result.has_value());

Следовательно, result() рассчитан на вызов после завершения coroutine body. Перед возвратом результата выполняется проверка сохранённого исключения через rethrow_if_exception().

Роль std::exception_ptr в этой реализации

Исключение, вышедшее из coroutine body, не выбрасывается наружу обычным способом в точке вызова coroutine-функции. Оно обрабатывается через promise.unhandled_exception() [1][2].

В данной реализации unhandled_exception() сохраняет исключение через exception = std::current_exception().

Позже result() вызывает rethrow_if_exception(), и сохранённое исключение повторно выбрасывается через std::rethrow_exception(...).

std::exception_ptr предназначен для хранения ссылки на объект исключения, который затем может быть повторно выброшен [8].

Для Task<T> при T != void сохранённое исключение повторно выбрасывается из result().

Для Task<void> метода result() нет; проверка сохранённого исключения выполняется явным вызовом rethrow_if_exception() после завершения coroutine body.

Владение coroutine handle

В классе хранится handle_type handle_, где handle_type — это std::coroutine_handle<promise_type>.

std::coroutine_handle является handle-типом, который ссылается на coroutine state, но сам по себе не задаёт RAII-владение этим состоянием [3].

Владение задаётся самим Task: копирование запрещено, перемещение разрешено, а деструктор вызывает destroy().

Копирование запрещено, потому что при разрешённом копировании два объекта Task могли бы хранить один и тот же coroutine handle и оба вызвать destroy() для одного coroutine state.

Перемещение разрешено, потому что при перемещении handle передаётся новому объекту, а исходный объект получает пустое значение через std::exchange(other.handle_, {}).

Следствие: в этой модели уничтожение coroutine state связано с одним объектом Task.

Условие для уничтожения coroutine state

destroy() вызывается только при наличии непустого handle. Внутри destroy() выполняется handle_.destroy(), после чего handle_ сбрасывается в пустое значение.

coroutine_handle::destroy() уничтожает coroutine state, на который ссылается handle [1][3].

Вызов handle_.destroy() в деструкторе Task имеет определённое поведение только при следующих условиях:

  • handle_ не был уничтожен через копию, полученную из native_handle();

  • coroutine не выполняется конкурентно;

  • coroutine находится в suspended state.

В стандарте указано, что вызов destroy() для coroutine, которая не находится в suspended state, приводит к undefined behavior [1].

Следовательно, владение Task, вызовы resume() и использование handle, полученного через native_handle(), должны быть согласованы внешним управляющим кодом.

native_handle()

Task сам не является scheduler-ом.

Но внешний scheduler или event loop должен иметь возможность получить coroutine handle, чтобы сохранить его и позже вызвать resume().

Для этого предоставлен метод native_handle(). Он возвращает копию std::coroutine_handle<promise_type>, но не передаёт владение coroutine state.

Уничтожение coroutine state в данной модели остаётся обязанностью объекта Task, потому что именно Task вызывает handle_.destroy().

Границы текущей реализации

Данная реализация задаёт return object coroutine-функции и lifetime coroutine state, но не задаёт awaiter protocol.

В классе отсутствуют await_ready(), await_suspend(...), await_resume() и operator co_await().

В C++ coroutine await-expression использует awaiter protocol, включающий await_ready, await_suspend и await_resume [2].

Следовательно, этот класс задаёт return object coroutine-функции Task<T> f();, но не задаёт поведение выражения co_await f().

Для поддержки co_await Task<T> требуется отдельное определение awaiter protocol.

Ограничение для ссылочных и неподдерживаемых типов результата

Для результата используется std::optional<T> result.

std::optional<T> содержит значение типа T как contained value [7].

Следовательно, данная реализация value-варианта определена только для таких T, для которых std::optional<T> является well-formed и для которых выражение result.emplace(std::forward<V>(value)) well-formed для operand конкретного co_return.

Task<T&>, Task<T[]>, Task<function-type> и другие формы, для которых std::optional<T> не может содержать contained value типа T, этой реализацией не поддерживаются.

Сводка типов

Для void-результата:

  • Task<> эквивалентен Task<void, StartSuspended>;

  • Task<void> эквивалентен Task<void, StartSuspended>;

  • Task<void, StartSuspended> использует void_promise, std::suspend_always, return_void();

  • Task<void, StartImmediately> использует void_promise, std::suspend_never, return_void().

Для результата-значения при T != void:

  • Task<T> эквивалентен Task<T, StartSuspended>;

  • Task<T, StartSuspended> использует value_promise<T>, std::suspend_always, return_value(...);

  • Task<T, StartImmediately> использует value_promise<T>, std::suspend_never, return_value(...).

Итоговая схема:

Task<>                         -> Task<void, StartSuspended>
Task<void>                     -> Task<void, StartSuspended>
Task<void, StartSuspended>     -> void_promise,     suspend_always, return_void()
Task<void, StartImmediately>   -> void_promise,     suspend_never,  return_void()

Task<T>                        -> Task<T, StartSuspended>, T != void
Task<T, StartSuspended>        -> value_promise<T>, suspend_always, return_value(...)
Task<T, StartImmediately>      -> value_promise<T>, suspend_never,  return_value(...)

Итоговая формулировка

execution_core::Task<T, StartPolicy> — это class template, который задаёт return object для C++20 coroutine-функций.

Параметр T определяет promise type: при T == void используется void_promise, при T != void используется value_promise<T>.

Параметр StartPolicy определяет результат initial_suspend(): StartSuspended даёт std::suspend_always, а StartImmediately даёт std::suspend_never.

Coroutine handle создаётся через std::coroutine_handle<Promise>::from_promise(...) и хранится внутри Task [1][3].

Coroutine state уничтожается деструктором Task через handle_.destroy() при наличии непустого handle [1][3].

В текущей реализации Task является владельцем coroutine handle и обеспечивает доступ к promise object после завершения coroutine body. Он не задаёт awaiter protocol для co_await Task<T>.

Корректное использование этой реализации требует, чтобы операции done(), resume() и destroy() вызывались только в состояниях coroutine, для которых эти операции имеют определённое поведение по стандарту.

Отличие от cppcoro

Этот Task не является заменой cppcoro.

cppcoro предоставляет набор coroutine-примитивов: task<T>, shared_task<T>, генераторы, async-generators, awaitable-типы и функции вроде sync_wait() и when_all().

Рассматриваемый здесь Task решает более узкую задачу: он показывает минимальную структуру пользовательского return object, который:

  • выбирает promise_type;

  • хранит std::coroutine_handle;

  • задаёт initial_suspend() и final_suspend();

  • сохраняет результат или исключение;

  • уничтожает coroutine state.

В этой реализации нет co_await Task<T>, нет sync_wait(), нет when_all(), нет scheduler-а и нет thread pool. Поэтому сравнивать её с cppcoro как с библиотекой нельзя; это минимальный строительный блок для собственного execution_core.

Источники

  1. ISO/IEC 14882:2020(E), разделы:

    • Coroutine definitions;

    • Coroutine promise;

    • Coroutine handle;

    • Coroutine state lifetime.

  2. cppreference: Coroutines https://en.cppreference.com/w/cpp/language/coroutines

  3. cppreference: std::coroutine_handle https://en.cppreference.com/w/cpp/coroutine/coroutine_handle

  4. cppreference: std::suspend_always https://en.cppreference.com/w/cpp/coroutine/suspend_always

  5. cppreference: std::suspend_never https://en.cppreference.com/w/cpp/coroutine/suspend_never

  6. cppreference: std::conditional https://en.cppreference.com/w/cpp/types/conditional

  7. cppreference: std::optional https://en.cppreference.com/w/cpp/utility/optional

  8. cppreference: std::exception_ptr https://en.cppreference.com/w/cpp/error/exception_ptr

Комментарии (10)


  1. mayorovp
    05.05.2026 11:36

    Налито много воды. Вода мокрая. Таким образом, в статье мокро, потому что налито много мокрой воды. На всякий случай напишу ещё раз, что воды много, вдруг кто не понял.

    Для кого вообще эта статья? Для кого расписаны все эти Task<T> эквивалентен Task<T, StartSuspended>, и почему банальныя фраза “значения по умолчанию для типов-параметров - void и StartSuspended” вообще нуждается в дальнейшей расшифровке?

    Зачем тут в статье вообще очевидные детали реализации, вроде того что метод return_void написан потому что компилятор его требует?

    Где примены использования вашего модуля? Где сравнение с сществующими реализациями (как минимум, с cppcoro)?


    1. olegiv2019 Автор
      05.05.2026 11:36

      Да, замечание принимаю. Статья получилась слишком подробной в местах, где достаточно одной строки, особенно про default template arguments и очевидные элементы promise type. Статья ориентирована не на пользователей готовых coroutine-библиотек, а на тех, кто пишет собственный минимальный coroutine return object под свой scheduler/event loop и хочет явно контролировать promise_type, std::coroutine_handle, initial_suspend(), final_suspend() и уничтожение coroutine state.


  1. mayorovp
    05.05.2026 11:36

    Теперь претензии к коду. Ваш класс Task не поддерживает оператор co_await. Более того, он не предоставляет вообще никакой возможности дождаться окончания сопрограммы. Но при этом окончание сопрограммы требуется для вызова любого из его методов, включая деструктор!

    Получается, этот код в принципе невозможно безопасно использовать. Нет, таких реализация нам не надо! Даже в качестве примера.


    1. olegiv2019 Автор
      05.05.2026 11:36

      Да, первая часть замечания верная: текущий Task не является awaitable object и не предоставляет самостоятельного механизма ожидания завершения. Данный класс — не аналог cppcoro::task, а минимальный return object и RAII-владелец std::coroutine_handle.

      Но утверждение, что для деструктора требуется именно окончание coroutine, неверно. Для coroutine_handle::destroy() требуется не final suspend, а suspended state. То есть coroutine может быть уничтожена и до завершения, например когда она находится в initial_suspend() или в другой точке приостановки. Завершение требуется для result(), но не для самого destroy().


      1. mayorovp
        05.05.2026 11:36

        Только вот уничтожение coroutine_handle в то время когда его позаимствовал чужой код, обрабатывающий co_await, приведёт к use after free. Уничтожать coroutine_handle в промежуточных точках можно только в тех случаях, когда все используемые awaitable могут корректно обработать такое уничтожение. А они не могут, потому что связать awaitable с промисом можно только через метод await_transform, которого у вас тоже нет.


        1. olegiv2019 Автор
          05.05.2026 11:36

          Описываемый Вами сценарий не является режимом использования показанного 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?».


          1. mayorovp
            05.05.2026 11:36

            Да, согласен, await_suspend может обращаться к promise напрямую.

            Однако, в вашем promise всё равно нет и одного публичного метода, которые можно было бы использовать для предотвращения проблем.

            В моих проектах такой механизм выносится в scheduler/awaiter-слой; это архитектурное решение, и в другой реализации оно может быть организовано иначе.

            У вас в вашем Task недостаточно возможностей чтобы этот scheduler/awaiter-слой сделал хоть что-то.


            1. olegiv2019 Автор
              05.05.2026 11:36

              В показанном Task действительно нет общего публичного интерфейса promise для cancellation/unregister-протокола, потому что такой протокол не входит в рассматриваемый уровень реализации.

              При этом scheduler/awaiter-слой не обязан строиться только через публичные методы promise. Он может быть реализован через конкретные awaitable-типы: awaiter хранит ссылку на scheduler и registration id, а при уничтожении coroutine state его деструктор удаляет сохранённый handle из внешней структуры ожидания.

              То есть такой механизм может находиться на уровне конкретного awaiter-а и scheduler-а, а не обязательно в самом Task или promise API.


  1. TarkWight
    05.05.2026 11:36

    Очередная gpt пародия на статью с таким же gpt генеративными ответами в комментариях... Когда нибудь такое будет подвергаться цензуре или запретам публикации?


    1. mayorovp
      05.05.2026 11:36

      Не, нейронки отвечают либо умнее, либо невпопад. Тут типичная протечка студенческой работы в интернет, когда студент выполнил семестровое задание и почему-то решил, что оно будет интересно кому-то кроме преподавателя. Ещё возможен вариант что это преподаватель тут пишет, в таком случае мне жаль его студентов.