Код: Выделить всё
throw;Я придумал следующее: в awaiter, возвращаемом из Initial_suspend, я возобновляю выполнение сопрограммы в await_suspend и просто позволяю ее исключениям распространяться обратно на вызывающую сторону. Просто!
Код: Выделить всё
struct InitialSuspend
{
bool await_ready(){ return false; }
void await_suspend(std::coroutine_handle
h)
{
h.resume();
h.promise().hasEverSuspended = true;
}
void await_resume(){}
};
К сожалению, тестируя это в различных компиляторах, я заметил ужасное несоответствие. Некоторые компиляторы уничтожают сопрограмму, когда вызов h.resume() генерирует исключение, тогда как другие компиляторы этого не делают. Учтите, что get_return_object может возвращать промежуточный тип, который ссылается на сопрограмму: он будет создан с использованием этой ссылки, поэтому теперь он наверняка владеет сопрограммой, верно? Но как ему узнать, когда следует уничтожить сопрограмму?
Я создал небольшую игровую программу с протоколированием, чтобы сравнить поведение GCC, Clang и MSVC: https://compiler-explorer.com/z/Yaanxoj9z
Результаты: GCC никогда не уничтожает саму сопрограмму, Clang уничтожает саму сопрограмму только тогда, когда get_return_object не совпадает с тип возвращаемого значения сопрограммы, и MSVC всегда пытается уничтожить саму сопрограмму. Это три совершенно разных варианта поведения.
Я думаю, что потенциально можно обойти эту проблему, заставив посредника связываться с деструктором промиса, чтобы, когда промис уничтожается, он мог сказать посреднику больше не уничтожать его. Но меня это не очень устраивает, мне нужно понять, почему это происходит и почему у каждого компилятора по-разному, на случай, если я каким-то образом вызываю неопределенное поведение.
Я попробовал прочитать стандарт сам, и в § 9.5.4, где обсуждаются сопрограммы, я вижу приблизительную схему преобразования компилятора сопрограммы:
Код: Выделить всё
{
promise-type promise promise-constructor-arguments ;
try {
co_await promise .initial_suspend() ;
function-body
} catch ( ... ) {
if (!initial-await-resume-called )
throw ;
promise .unhandled_exception() ;
}
final-suspend :
co_await promise .final_suspend() ;
}
Поскольку мой unhandled_Exception безоговорочно повторно
Код: Выделить всё
throw;Я думаю, что основное различие здесь заключается в том, завершает ли первоначальный ожидающий приостановки await_suspend обычный возврат или через исключение, но, похоже, это не подробно описано в стандарте, по крайней мере, я не могу найти. Я знаю, что при нормальных обстоятельствах выход из await_suspend через исключение должен возобновить работу сопрограммы и повторно выдать исключение, а возобновление сопрограммы, которая уже находится в конечной точке приостановки, является неопределенным поведением, но у меня сложилось впечатление, что начальная точка приостановки обрабатывается специально для подобных ситуаций.
В моей тестовой программе, если я добавлю throw 1; перед h.resume();, все три компилятора соглашаются, что именно они должны уничтожить сопрограмму, а промежуточный тип в конечном итоге выполняет двойное уничтожение. Это еще раз усиливает идею о том, что промис должен быть уничтожен компилятором. Я предполагаю, что обманом GCC и Clang является вызов .resume() внутри await_suspend перед его завершением через исключение. Это дополняет теорию о том, что я вызываю неопределенное поведение, и в этом случае мне придется использовать обходной путь, отличный от того, который я предложил ранее.
Второй обходной путь, который я придумал, — это вместо этого симметричный переход к сопрограмме мониторинга, единственной задачей которой является вызов .resume(). Таким образом, мы вышли из await_suspend начального ожидания, вернувшись в обычном режиме, и исключение все равно может вернуться к вызывающей стороне исходной функции задачи. Но этот подход означает, что сопрограмма мониторинга также должна повторно генерировать исключение из своего unhandled_Exception, что означает, что каждая сопрограмма мониторинга является одноразовой и должна быть заново перераспределена для каждой нормальной сопрограммы. Это ужасно раздражает, когда все, что я действительно хочу знать, это то, полностью ли приостанавливается сопрограмма в какой-либо момент, и кажется примерно равным по сложности исходному подходу с использованием await_transform для отслеживания ее вручную, но подход сопрограммы мониторинга, по крайней мере, исключит всю неэффективную бухгалтерию из каждого ожидающего. Однако мне бы очень хотелось не использовать ни один из этих подходов, первоначальный трекер приостановки кажется таким элегантным по сравнению с ним.
Код: Выделить всё
std::coroutine_handle await_suspend(std::coroutine_handle
h)
{
Monitor monitor([](std::coroutine_handle h)
-> Monitor
{
h.resume();
h.promise().everSuspended = true;
co_return;
}(h));
return std::exchange(monitor.h, {});
}
Это оказалось несколько хакерским решением, и результаты были как лучше, так и хуже. GCC и Clang по-прежнему ведут себя по-разному, но, по крайней мере, нет утечек или двойного уничтожения, а MSVC обнаруживает внутреннюю ошибку компилятора, поэтому я понятия не имею, что он будет делать.
Что здесь происходит? Кто на самом деле должен нести ответственность за уничтожение сопрограммы в исходном случае? Действительно ли я вызываю неопределенное поведение? Могу ли я что-нибудь сделать, чтобы узнать, приостанавливалась ли когда-либо сопрограмма, кроме ручного учета с помощью await_transform?
Подробнее здесь: https://stackoverflow.com/questions/798 ... om-its-ini
Мобильная версия