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

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

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

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

Ответ 1

Проблема, которую вы цитируете, происходит, когда ваш деструктор выполняется как часть процесса разворачивания стека (когда ваш объект не был создан как часть раскрутки стека) 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 для обнаружения этого случая. Это крайне ошибочно.

Ответ 2

Существует одно использование для 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

Ответ 3

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

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

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

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

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

Ответ 4

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

Ответ 5

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