Правильно завершающая программа. Использование исключений
Вопрос: Является ли использование исключений подходящим способом для завершения моей программы, если все, что я хочу, это показать сообщение об ошибке и закрыть (учитывая, что я могу быть глубоко в программе)? Могу ли я просто явно вызвать что-то вроде exit() вместо?
Что я сейчас делаю:
Я работаю над игровым проектом и пытаюсь найти лучший способ прекратить работу программы в случае ошибки, требующей такого действия. Например, в случае, когда текстуры не могут быть загружены, отображается сообщение об ошибке и завершается выполнение программы.
В настоящее время я делаю это с такими исключениями:
int main()
{
Game game;
try
{
game.run();
}
catch (BadResolutionException & e)
{
Notification::showErrorMessage(e.what(), "ERROR: Resolution");
return 1;
}
catch (BadAssetException & e)
{
Notification::showErrorMessage(e.what(), "ERROR: Assets");
return 1;
}
catch (std::bad_alloc & e)
{
Notification::showErrorMessage(e.what(), "ERROR: Memory");
return 1;
}
return 0;
}
Все, кроме bad_alloc, являются моими собственными определенными исключениями, полученными из runtime_error.
Мне не нужна ручная очистка ресурсов, и я использую std:: unique_ptr для любого динамического выделения. Мне просто нужно отобразить сообщение об ошибке и закрыть программу.
Исследования/альтернативы исключениям:
Я просмотрел много сообщений о SO и других местах, и видел, как другие говорят что-либо из того, что не используют исключения, использовать исключения, но вы неправильно их используете. Я также посмотрел прямо на вызов, например, exit().
Использование exit() звучит неплохо, но я читаю, что он не вернется через стек вызовов до основной очистки всех вещей (если я смогу найти это снова, я отправлю ссылку). Кроме того, согласно http://www.cplusplus.com/reference/cstdlib/exit/, это не должно использоваться, если несколько потоков активны. Я ожидаю создать второй поток в течение короткого времени хотя бы один раз, и в этом потоке может произойти ошибка.
Не использование исключений упоминалось в некоторых ответах здесь относительно игр https://gamedev.stackexchange.com/info/103285/how-industy-games-handle-their-code-errors-and-exceptions
Использовать исключения обсуждалось здесь: http://www.quora.com/Why-do-some-people-recommend-not-using-exception-handling-in-C++
Есть несколько других источников, которые я прочитал, но это были самые последние, на которые я смотрел.
Личный вывод:
Из-за моего ограниченного опыта работы с обработкой ошибок и использованием исключений я не уверен, что я на правильном пути. Я выбрал маршрут использования исключений на основе кода, который я написал выше. Если вы согласны с тем, что я должен решать эти случаи с исключениями, правильно ли использую их?
Ответы
Ответ 1
Обычно считается хорошей практикой, чтобы все исключения распространялись до main
. Это прежде всего потому, что вы можете быть уверены, что стек правильно размотан, и все деструкторы вызываются (см. этот ответ). Я также считаю его более организованным, чтобы делать это таким образом; вы всегда знали, где закончится ваша программа (если программа не сработает). Это также облегчает более последовательное сообщение об ошибках (часто игнорируется в обработке исключений, если вы не можете справиться с этим исключением, вы должны убедиться, что ваш пользователь точно знает почему). Если вы всегда начинаете с этого основного макета
int main(int argc, const char **argv)
{
try {
// do stuff
return EXIT_SUCCESS;
} catch (...) {
std::cerr << "Error: unknown exception" << std::endl;
return EXIT_FAILURE;
}
}
тогда вы не ошибетесь. Вы можете (и должны) добавлять конкретные операторы catch
для улучшения отчетов об ошибках.
Исключения при многопоточности
Существует два основных способа выполнения асинхронного выполнения кода в С++ 11 с использованием стандартных функций библиотеки: std::async
и std::thread
.
Сначала простой. std::async
вернет a std::future
, который будет захватывать и хранить любые неперехваченные исключения, брошенные в данной функции. Вызов std::future::get
в будущем вызовет любые исключения для распространения в вызывающем потоке.
auto fut = std::async(std::launch::async, [] () { throw std::runtime_error {"oh dear"}; });
fut.get(); // fine, throws exception
С другой стороны, если исключение в объекте std::thread
не показано, тогда будет вызываться std::terminate
:
try {
std::thread t {[] () { throw std::runtime_error {"oh dear"};}};
t.join();
} catch(...) {
// only get here if std::thread constructor throws
}
Одним из решений этого может быть передача std::exception_ptr
в объект std::thread
, который может передать исключение:
void foo(std::exception_ptr& eptr)
{
try {
throw std::runtime_error {"oh dear"};
} catch (...) {
eptr = std::current_exception();
}
}
void bar()
{
std::exception_ptr eptr {};
std::thread t {foo, std::ref(eptr)};
try {
// do stuff
} catch(...) {
t.join(); // t may also have thrown
throw;
}
t.join();
if (eptr) {
std::rethrow_exception(eptr);
}
}
Хотя лучший способ - использовать std::package_task
:
void foo()
{
throw std::runtime_error {"oh dear"};
}
void bar()
{
std::packaged_task<void()> task {foo};
auto fut = task.get_future();
std::thread t {std::move(task)};
t.join();
auto result = fut.get(); // throws here
}
Но если у вас нет оснований использовать std::thread
, предпочитайте std::async
.
Ответ 2
Нет ничего плохого в том, что вы поймаете неустранимые ошибки и остановите свою программу таким образом. Фактически, как следует использовать исключения. Однако будьте осторожны, чтобы не пересекать линию использования исключений для управления потоком вашей программы в обычных условиях. Они должны всегда представлять ошибку, которая не может быть изящно обработана на уровне ошибки.
Вызов exit()
не будет разворачивать стек из того места, где вы его вызывали. Если вы хотите выйти чисто, то, что вы уже делаете, идеально.
Ответ 3
Вы уже приняли ответ, но я хотел добавить что-то об этом:
Можно ли просто явно вызвать что-то вроде exit() вместо?
Вы можете вызвать exit, но (возможно) не должны.
std::exit
следует зарезервировать для ситуаций, когда вы хотите выразить "выход прямо сейчас!", а не просто "приложение ничего не остается".
В качестве примера, если вы должны были написать контроллер для лазера, используемого при лечении рака, ваш первый приоритет в случае, если что-то пошло не так, - это закрыть лазер и вызвать std::exit
- или, возможно, std::terminate
(to убедитесь, что любые побочные эффекты висячего, медленного или аварийного приложения не убивают пациента).
Подобно тому, как исключения не должны использоваться для управления потоком приложения, exit
не следует использовать, чтобы остановить приложение в нормальных условиях.
Ответ 4
Является ли использование исключений подходящим способом для завершения моей программы, если все, что я хочу, это отобразить сообщение об ошибке и закрыть (учитывая, что я могу быть глубоко в программе)?
Да. Это причина использования исключений. Произошла ошибка в коде, и что-то на более высоком уровне справится с этим. В вашем случае на самом высоком уровне.
Есть аргументы для/против исключений против кодов ошибок, и это хорошо читать:
Исключения или коды ошибок
Можно ли просто явно вызвать что-то вроде exit() вместо?
Вы можете, но вы можете в конечном итоге дублировать свой код ведения журнала. Кроме того, если в будущем вы решите, что хотите обрабатывать исключение по-разному, вам придется изменить все ваши вызовы на выход. Представьте, что вам нужно другое сообщение или вернуться к альтернативному процессу.
Другой аналогичный вопрос:
Правильное использование exit() в С++?
У вас также есть недостаток в вашем подходе, поскольку вы не обрабатываете все исключения (С++). Вы хотите что-то вроде этого:
int main()
{
Game game;
try
{
game.run();
}
catch (BadResolutionException & e)
{
Notification::showErrorMessage(e.what(), "ERROR: Resolution");
return 1;
}
catch (BadAssetException & e)
{
Notification::showErrorMessage(e.what(), "ERROR: Assets");
return 1;
}
catch (std::bad_alloc & e)
{
Notification::showErrorMessage(e.what(), "ERROR: Memory");
return 1;
}
catch (...)
{
// overload?
Notification::showErrorMessage("ERROR: Unhandled");
return 1;
}
return 0;
}
Если вы не обрабатываете все * исключения, вы можете завершить игру, не сообщив ничего полезного.
Вы не можете обрабатывать ВСЕ исключения. См. Эту ссылку:
С++ ловит все исключения
Ответ 5
Из документации:
[[noreturn]] void exit (статус int); Завершить процесс вызова Обычно завершает процесс, выполняя регулярную очистку для завершающих программ.
Нормальное завершение программы выполняет следующее (в том же порядке):
Объекты, связанные с текущим потоком с длительностью хранения потока, уничтожаются (только С++ 11).
Объекты со статической продолжительностью хранения уничтожаются (С++) и вызываются функции, зарегистрированные с помощью atexit.
Все C-потоки (открытые с функциями) закрываются (и очищаются, если они буферизованы), и все файлы, созданные с помощью tmpfile, удаляются.
Элемент управления возвращается в среду хоста.
Обратите внимание, что объекты с автоматическим хранилищем не уничтожаются, вызывая exit (С++).
Если статус равен нулю или EXIT_SUCCESS, успешное состояние завершения возвращается в среду хоста.
Если статус EXIT_FAILURE, в состояние хоста возвращается неуспешный статус завершения.
В противном случае возвращаемый статус зависит от реализации системы и библиотеки.
Для аналогичной функции, которая не выполняет описанную выше очистку, см. quick_exit.