C ++ 11 — Почему C ++ не использует std :: nested_exception, чтобы разрешить выбрасывание из деструктора?

Основная проблема с выбрасыванием исключений из деструктора состоит в том, что в момент вызова деструктора другое исключение может быть «в полете» (std::uncaught_exception() == true) и так не очевидно, что делать в таком случае. «Перезапись» старого исключения новым будет одним из возможных способов справиться с этой ситуацией. Но было решено, что std::terminate (или другой std::terminate_handler) должен быть вызван в таких случаях.

C ++ 11 представил функцию вложенных исключений через std::nested_exception учебный класс. Эта функция может быть использована для решения проблемы, описанной выше. Старое (необработанное) исключение может быть просто вложено в новое исключение (или наоборот?), А затем это вложенное исключение может быть сгенерировано. Но эта идея не была использована. std::terminate до сих пор вызывается в такой ситуации в C ++ 11 и C ++ 14.

Итак, вопросы. Была ли рассмотрена идея с вложенными исключениями? Есть ли с этим проблемы? Не изменится ли ситуация в C ++ 17?

13

Решение

Проблема, на которую вы ссылаетесь, возникает, когда ваш деструктор выполняется как часть процесса разматывания стека (когда ваш объект не был создан как часть разматывания стека)1, и ваш деструктор должен выдать исключение.

Так как это работает? У вас есть два исключения в игре. исключение X это тот, который заставляет стек раскручиваться. исключение Y это тот, который хочет бросить деструктор. nested_exception может только держать один из них.

Так что, возможно, у вас есть исключение Y содержать nested_exception (или, может быть, просто exception_ptr). Итак … как вы справляетесь с этим на catch сайт?

Если вы ловите Yи это случается иметь некоторые встроенные X, Как вы его получите? Помните: exception_ptr является Тип стертым; кроме того, чтобы передать его, единственное, что вы можете сделать с ним, это отбросить его. Так что люди должны делать это:

catch(Y &e)
{
if(e.has_nested())
{
try
{
e.rethrow_nested();
}
catch(X &e2)
{
}
}
}

Я не вижу много людей, делающих это. Тем более что будет очень много возможных X-es.

1Пожалуйста, не используйте std::uncaught_exception() == true обнаружить этот случай. Это крайне некорректно.

8

Другие решения

Есть одно использование для std::nested exceptionи только одно использование (насколько мне удалось обнаружить).

Сказав это, это фантастика, я использую вложенные исключения во всех своих программах, и в результате время, потраченное на поиск неясных ошибок, практически равно нулю.

Это связано с тем, что вложенные исключения позволяют легко создавать полностью аннотированный стек вызовов, который генерируется в точке ошибки, без каких-либо накладных расходов во время выполнения, не нужно много журналировать во время повторного запуска (что в любом случае изменит время), и не загрязняя логику программы обработкой ошибок.

например:

#include <iostream>
#include <exception>
#include <stdexcept>
#include <sstream>
#include <string>

// this function will re-throw the current exception, nested inside a
// new one. If the std::current_exception is derived from logic_error,
// this function will throw a logic_error. Otherwise it will throw a
// runtime_error
// The message of the exception will be composed of the arguments
// context and the variadic arguments args... which may be empty.
// The current exception will be nested inside the new one
// @pre context and args... must support ostream operator <<
template<class Context, class...Args>
void rethrow(Context&& context, Args&&... args)
{
// build an error message
std::ostringstream ss;
ss << context;
auto sep = " : ";
using expand = int[];
void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });
// figure out what kind of exception is active
try {
std::rethrow_exception(std::current_exception());
}
catch(const std::invalid_argument& e) {
std::throw_with_nested(std::invalid_argument(ss.str()));
}
catch(const std::logic_error& e) {
std::throw_with_nested(std::logic_error(ss.str()));
}
// etc - default to a runtime_error
catch(...) {
std::throw_with_nested(std::runtime_error(ss.str()));
}
}

// unwrap nested exceptions, printing each nested exception to
// std::cerr
void print_exception (const std::exception& e, std::size_t depth = 0) {
std::cerr << "exception: " << std::string(depth, ' ') << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch (const std::exception& nested) {
print_exception(nested, depth + 1);
}
}

void really_inner(std::size_t s)
try      // function try block
{
if (s > 6) {
throw std::invalid_argument("too long");
}
}
catch(...) {
rethrow(__func__);    // rethrow the current exception nested inside a diagnostic
}

void inner(const std::string& s)
try
{
really_inner(s.size());

}
catch(...) {
rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}

void outer(const std::string& s)
try
{
auto cpy = s;
cpy.append(s.begin(), s.end());
inner(cpy);
}
catch(...)
{
rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic
}int main()
{
try {
// program...
outer("xyz");
outer("abcd");
}
catch(std::exception& e)
{
// ... why did my program fail really?
print_exception(e);
}

return 0;
}

ожидаемый результат:

exception: outer : abcd
exception:  inner : abcdabcd
exception:   really_inner
exception:    too long

Объяснение линии расширения для @Xenial:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

args — это пакет параметров. Он представляет 0 или более аргументов (ноль важен).

Мы хотим, чтобы компилятор расширил пакет аргументов для нас, в то же время создавая полезный код.

Давайте возьмем это снаружи в:

void(...) — означает что-то оценить и выбросить результат — но оценить его.

expand{ ... };

Вспоминая это expand является typedef для int [], это означает, что давайте оценим целочисленный массив.

0, (...)...;

означает, что первое целое число равно нулю — помните, что в c ++ недопустимо определять массив нулевой длины. Что если args … представляет 0 параметров? Этот 0 гарантирует, что в массиве есть как минимум одно целое число.

(ss << sep << args), sep = ", ", 0);

использует оператор запятой для оценки последовательности выражений по порядку, принимая результат последнего. Выражения:

s << sep << args — вывести разделитель с последующим текущим аргументом в поток

sep = ", " — затем установите разделитель на запятую + пробел

0 — привести к значению 0. Это значение, которое входит в массив.

(xxx params yyy)... — означает сделать это один раз для каждого параметра в пакете параметров params

Следовательно:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

означает «для каждого параметра в params, выведите его в ss после печати разделителя. Затем обновите разделитель (чтобы у нас был другой разделитель для первого). Сделайте все это как часть инициализации воображаемого массива, который мы затем бросим далеко.

6

Вложенные исключения просто добавляют скорее всего игнорируемую информацию о том, что произошло, а именно:

Было сгенерировано исключение X, стек разматывается, т. Е. Деструкторы локальных объектов вызываются с этим исключением «в полете», а деструктор одного из этих объектов в свою очередь выдает исключение Y.

Обычно это означает, что очистка не удалась.

И тогда это не ошибка, которую можно исправить, сообщив об этом вверх и позволив коду более высокого уровня решить, например, использовать некоторые альтернативные средства для достижения своей цели, потому что объект, который содержал информацию, необходимую для очистки, был уничтожен, вместе с его информацией, но без очистки. Так что это очень похоже на провал утверждения. Состояние процесса может быть очень плохим, нарушая предположения кода.

Деструкторы, которые бросают, в принципе могут быть полезны, например, В качестве идеи Андрей однажды сообщил о неудачной транзакции при выходе из области видимости блока. То есть при нормальном выполнении кода локальный объект, который не был проинформирован об успешности транзакции, может выбросить его деструктор. Это становится проблемой только тогда, когда происходит конфликт с правилом C ++ для исключения во время разматывания стека, где это требует обнаружение о том, может ли быть выдано исключение, что представляется невозможным. В любом случае, деструктор используется только для автоматического вызова, а не в роли очистки. И поэтому можно сделать вывод, что текущие правила C ++ предполагают роль очистки для деструкторов.

2

Настоящая проблема в том, что метание от деструкторов — логическая ошибка. Это похоже на определение оператора + () для выполнения умножения.
Деструкторы не должны использоваться в качестве хуков для запуска произвольного кода. Их цель — детерминистически высвободить ресурсы. По определению, это не должно провалиться. Все остальное нарушает предположения, необходимые для написания общего кода.

1

Проблема, которая может возникнуть при разматывании стека с цепочкой исключений из деструкторов, заключается в том, что вложенная цепочка исключений может быть слишком длинной. Например, у вас есть std::vector из 1 000 000 элементы, каждый из которых создает исключение в своем деструкторе. Давайте предположим, деструктор std::vector собирает все исключения из деструкторов своих элементов в единую цепочку вложенных исключений. Тогда результирующее исключение может быть даже больше оригинального std::vector контейнер. Это может вызвать проблемы с производительностью и даже бросать std::bad_alloc во время раскручивания стека (которое даже не может быть вложено, потому что для этого недостаточно памяти) или выбрасывание std::bad_alloc в других не связанных местах в программе.

1
По вопросам рекламы [email protected]