Следует ли когда-либо создавать объект результата сопрограммы после initial_suspend ()? - PullRequest
3 голосов
/ 10 июля 2020

Я писал библиотеку сопрограмм и столкнулся со специфической проблемой. В некоторых случаях создание объекта результата сопрограммы было упорядочено после вызова initial_suspend.

Вопрос: является ли это упорядочение ошибкой со стороны компилятора?

Фон

В моем конкретном случае это вызывало ошибку sh, поскольку обещание генератора выполнялось в предположении, что у него нет владельца.

Соответствующий раздел стандарта C ++ 20 - это раздел 9.5.4.7, в котором говорится:

Выражение promise.get_return_object() используется для инициализации результата glvalue или объекта результата prvalue вызова сопрограммы. . Вызов get_return_object упорядочивается перед вызовом initial_suspend и вызывается не более одного раза.

Когда я читал эту часть стандарта, я изначально интерпретировал это как означающее, что инициализация объект результата сопрограммы упорядочивается сразу после вызова promise.get_return_object(): когда вы инициализируете что-то, инициализация происходит сразу после вычисления аргументов конструктора, а не как отложенный эффект.

К сожалению, это не то поведение, которое я наблюдал. Источник cra sh заключается в том, что инициализация объекта результата была упорядочена после начальной приостановки , несмотря на то, что вызов promise.get_return_object() был упорядочен до начальной приостановки.

Наблюдение это поведение в коде ( см. живой пример здесь )

Давайте напишем очень простой тип, который может быть возвращен из сопрограммы:

template <class Promise>
struct coroutine {
    std::coroutine_handle<Promise> handle;
    using promise_type = Promise;
    coroutine(std::coroutine_handle<Promise> p) : handle(p) {
        std::cout << "  Running coroutine(std::coroutine_handle<Promise> p)\n";
    }

    ~coroutine() {
        if (handle) {
            handle.destroy();
        }
    }
};

Потому что coroutine<Promise> может быть построенным из std::coroutine_handle<Promise>, вызов promise.get_return_object() может возвращать либо std::coroutine_handle<Promise>, который используется для создания coroutine, либо он может возвращать coroutine<Promise> напрямую.

Давайте напишем два разные типы обещаний, по одному для каждой опции:

struct promise_base {
    std::suspend_never initial_suspend() {
        std::cout << "  Running initial_suspend()\n";
        return {};
    }
    std::suspend_always final_suspend() { return {}; }
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
};

struct promise_A : promise_base {
    using handle = std::coroutine_handle<promise_A>;
    handle get_return_object() {
        std::cout << "  Running get_return_object()\n";
        return handle::from_promise(*this);
    }
};
struct promise_B : promise_base {
    using handle = std::coroutine_handle<promise_B>;
    coroutine<promise_B> get_return_object() {
        std::cout << "  Running get_return_object()\n";
        return {handle::from_promise(*this)};
    }
};

Затем мы можем написать две идентичные сопрограммы на основе promise_A и promise_B:

coroutine<promise_A> run_A() {
    std::cout << "Inside coroutine body\n";
    co_return;
}
coroutine<promise_B> run_B() {
    std::cout << "Inside coroutine body\n";
    co_return;
}

В этом коде

  • promise_A возвращает дескриптор из get_return_object, и он используется для создания объекта результата типа coroutine<promise_A>.
  • promise_B напрямую возвращает coroutine<promise_B>, который возвращается как объект результата.

В случае promise_A, построение объекта результата coroutine<promise_A> упорядочивается после вызова initial_suspend ().

Мы можем написать следующий тестовый код, чтобы проверить это:

void test_promise_A() {
    std::cout << "------ Testing A ------\n";
    run_A();
    std::cout << "\n\n";
}
void test_promise_B() {
    std::cout << "------ Testing B ------\n";
    run_B();
    std::cout << "\n\n";
}
int main() {
    test_promise_A();
    test_promise_B();
}

В G CC 10.1 это дает следующий результат. Обратите внимание на разницу в последовательности между Testing A и Testing B.

------ Testing A ------
  Running get_return_object()
  Running initial_suspend()
Inside coroutine body
  Running coroutine(std::coroutine_handle<Promise> p)


------ Testing B ------
  Running get_return_object()
  Running coroutine(std::coroutine_handle<Promise> p)
  Running initial_suspend()
Inside coroutine body

Повторим вопрос: Является ли последовательность Testing A ошибкой?

...