Кто несет ответственность за уничтожение сопрограммы C++20, которая выдает ошибку после первоначального приостановки?C++

Программы на C++. Форум разработчиков
Ответить
Anonymous
 Кто несет ответственность за уничтожение сопрограммы C++20, которая выдает ошибку после первоначального приостановки?

Сообщение Anonymous »

Я пытаюсь отслеживать, приостанавливалась ли когда-либо сопрограмма C++20, чтобы unhandled_Exception знал, может ли она просто повторно выполнить вернуть исключение вызывающей стороне исходной функции сопрограммы или, если ему необходимо вызвать std::current_Exceptionion() и сохранить его в обещании для последующей проверки. Мой обычный подход к этому — преобразовать каждый ожидающий элемент с помощью await_transform и вручную проверить возвращаемые значения и выдачу исключений await_suspend, чтобы решить, что представляет собой приостановку. Однако это чревато ошибками и неэффективно, поэтому я пошел искать лучший способ.
Я придумал следующее: в 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() ;
}
Ниже начальный-await-resume-called описывается как «изначально ложный и устанавливается в значение true непосредственно перед вычислением выражения await-resume начального выражения await». Кажется, он защищает от вызова unhandled_Exception, если сопрограмма не была возобновлена ​​с начальной точки приостановки. В моей тестовой программе unhandled_Exception вызывается во всех случаях, так что, похоже, это указывает на то, что компиляторы знают, что мы прошли начальную точку приостановки, когда исключение генерируется.
Поскольку мой unhandled_Exception безоговорочно повторноявляется исключением, Final_suspend никогда не обрабатывается. Поскольку тип обещания в приведенном выше преобразовании находится в той же области, что и окончательная обработка приостановки, мне кажется, что обещание, вероятно, всегда должно автоматически уничтожаться компилятором в этом случае, но ввод тела await_suspend и возобновление сопрограммы через .resume() влияют на интерпретацию этого способами, которые мне неясны, и, вероятно, поэтому другие компиляторы ведут себя по-другому. Например, в моей тестовой программе перемещение оператора throw 1; после co_await std::suspend_always{}; приводит к совершенно нормальному и согласованному поведению во всех трех компиляторах: компиляторы никогда не уничтожают сопрограмму, оставляя ответственность за мой пользовательский код.
Я думаю, что основное различие здесь заключается в том, завершает ли первоначальный ожидающий приостановки 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, {});
}
Я опробовал этот второй обходной путь в программе тестовой игровой площадки: https://compiler-explorer.com/z/EG8hxxc7M
Это оказалось несколько хакерским решением, и результаты были как лучше, так и хуже. GCC и Clang по-прежнему ведут себя по-разному, но, по крайней мере, нет утечек или двойного уничтожения, а MSVC обнаруживает внутреннюю ошибку компилятора, поэтому я понятия не имею, что он будет делать.
Что здесь происходит? Кто на самом деле должен нести ответственность за уничтожение сопрограммы в исходном случае? Действительно ли я вызываю неопределенное поведение? Могу ли я что-нибудь сделать, чтобы узнать, приостанавливалась ли когда-либо сопрограмма, кроме ручного учета с помощью await_transform?

Подробнее здесь: https://stackoverflow.com/questions/798 ... om-its-ini
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C++»