Восстановить контекст из исключения - PullRequest
0 голосов
/ 02 февраля 2019

Рассмотрим следующий класс управления ресурсами

class FooResouce
    {
    public:
        explicit FooResouce(T arg_to_construct_with)
            {
            m_foo = create_foo_resouce(arg_to_construct_with);
            if(m_foo == nullptr)
                {throw SomeException(get_foo_resource_error(), arg_to_construct_with);}
            }
        // Dtor and move operations
        // Other FooResource interaction methods
    private:
        foo_resource_t* m_foo;
    };

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

  1. Передать некоторую необязательную информацию о контексте в качестве дополнительного аргумента Ctor, который затем можно сохранить в исключении
  2. На сайте вызовов используйте функцию pushContext.Эта функция будет хранить контекст, используя локальное хранилище потока.
  3. Catch and rethrow.Я думаю, что код был бы уродлив в этом случае.

1 Ответ

0 голосов
/ 02 февраля 2019

Хотя это решение противоречит вашему третьему требованию 3: нет Catch and rethrow , я предлагаю решение с std::nested_exception и макросами, потому что это, кажется, дает разумное решение для текущей проблемы, по крайней мере мне,Я надеюсь, что этот слишком длинный ответ может помочь вам.


1.Обработка ошибок с std::nested_exception


Во-первых, мы можем рекурсивно вкладывать исключения, используя std::nested_exception.Грубо говоря, мы можем добавить исключения произвольных типов в этот класс, вызвав std::throw_with_nested.Это позволяет нам переносить всю информацию о сгенерированных исключениях с помощью довольно простого кода, просто выбрасывая каждое исключение на std::throw_with_nested в каждом обработчике перехвата многоточия catch(…){ } в на верхнем уровне .

Дляэкземпляр, следующая функция h выдает std::nested_exception, который агрегирует определяемое пользователем исключение SomeException и std::runtime_error:

struct SomeException : public std::logic_error {
    SomeException(const std::string& message) : std::logic_error(message) {}
};

[[noreturn]] void f(){
    std::throw_with_nested(SomeException("error."));
}

[[noreturn]] void g()
{
    try {
        f();
    }
    catch (...) {
        std::throw_with_nested(std::runtime_error("Error of f."));
    }
};

[[noreturn]] void h()
{
    try {
        g();
    }
    catch (...) {
        std::throw_with_nested(std::runtime_error("Error of g."));
    }    
}

Обнаружение исключений (базовый уровень)

Заменив все эти std::throw_with_nested следующей функцией throw_with_nested_wrapper через макрос THROW_WITH_NESTED, мы можем сделать записи имен файлов и номеров строк, где произошли исключения.Как известно, __FILE__ и __LINE__ предварительно определены стандартом C ++.Таким образом, макрос THROW_WITH_NESTED играет ключевую роль в добавлении этой информации о местоположении:

// "..." are arguments of the ctor of ETYPE
// and the first one must be a string literal.
#define THROW_WITH_NESTED(ETYPE, ...)  \
throw_with_nested_wrapper<ETYPE>(__FILE__, __LINE__, __VA_ARGS__);

template<typename E, typename ...Args>
[[noreturn]]
void throw_with_nested_wrapper(
    char const* fileName,
    std::size_t line,
    const std::string& message,
    Args&& ...args)
{
    auto info = std::string(fileName)
                         + ", l." + std::to_string(line) + ", " 
                         + message;

    std::throw_with_nested(E(info, std::forward<decltype(args)>(args)...));
};

Определение исключений (верхний уровень)

Если нам нужноподобрать информацию о том, где исключение было вызвано в верхнем уровне , следующий макрос HOOK повторного использования вышеуказанного макроса THROW_WITH_NESTED будет работать для нас:

#define HOOK(OPERATION)                                         \
[&]()                                                           \
{                                                               \
    try{                                                        \
        return OPERATION;                                       \
    }                                                           \
    catch(...){                                                 \
        auto info = std::string(#OPERATION) + ", upper level."; \
        THROW_WITH_NESTED(std::runtime_error, info);            \
    }                                                           \
}()

Наконец, сначалатри функции f, g и h переписаны и упрощены следующим образом:

[[noreturn]] void f(){
    THROW_WITH_NESTED(SomeException, "SomeException, fundamental level.");
}

void g(){
    HOOK(f());
};

void h(){
    HOOK(g());
}

Извлечение информации об ошибках

Извлечение всех поясненийИнформация о вложенных исключениях является простой задачей.Передав пойманное исключение в самом внешнем блоке try-catch в следующую функцию output_exceptions_impl, мы можем это сделать.Каждое вложенное исключение может быть рекурсивно выдано на std::nested_exception::rethrow_nested.Так как эта функция-член вызывает std::terminate, когда нет сохраненного исключения, мы должны применить dynamic_cast, чтобы избежать его, как указано в этом post:

template<typename E>
std::enable_if_t<std::is_polymorphic<E>::value>
rethrow_if_nested_ptr(const E& exception)
{
    const auto *p = 
        dynamic_cast<const std::nested_exception*>(std::addressof(exception));

    if (p && p->nested_ptr()){
        p->rethrow_nested();
    }
}

void output_exceptions_impl(
    const std::exception& exception,
    std::ostream& stream,
    bool isFirstCall = false)
{
    try 
    {
        if (isFirstCall) { throw; }

        stream << exception.what() << std::endl;
        rethrow_if_nested_ptr(exception);
    }
    catch (const std::runtime_error& e) {
        stream << "Runtime error: ";
        output_exceptions_impl(e, stream);
    }
    /* ...add further catch-sections here... */
    catch(...){
        stream << "Unknown Error.";
    }
}

Кстати, явные try-catch блоки в самых внешних местах довольно многословны, и поэтому я обычно использую следующий макрос, предложенный в this post:

#define CATCH_BEGIN          try{
#define CATCH_END(OSTREAM)   } catch(...) { output_exceptions(OSTREAM); }

void output_exceptions(std::ostream& stream)
{
    try {
        throw;
    }
    catch (const std::exception& e) {
        output_exceptions_impl(e, stream, true);
    }
    catch (...) {
        stream << "Error: Non-STL exceptions." << std::endl;
    }
}

Тогда все исключения, выданные из h, можно отследить и распечатать с помощью следующего кода.Вставляя макросы THROW_WITH_NESTED, HOOK, CATCH_BEGIN и CATCH_END в правильные строки нашего кода, мы можем найти исключения в каждом потоке:

CATCH_BEGIN // most outer try-catch block in each thread
...

HOOK(h());
...

CATCH_END(std::cout)

Затем мы получим следующий вывод, гдеимена файлов и номера строк являются лишь примерами.Вся доступная информация записывается:

DEMO с 2 потоками

Ошибка времени выполнения: prog.cc, l.119, h(), верхний уровень.

Ошибка времени выполнения: prog.cc, l.113, g (), верхний уровень.

Ошибка времени выполнения: prog.cc, l.109, f (), верхний уровень.

Логическая ошибка: prog.cc, l.105, SomeException, фундаментальный уровень.


2.В случае FooResouce


Первое требование:

  1. Передать некоторую необязательную контекстную информацию в качестве дополнительного аргумента Ctor, который затем можно сохранить висключение

Давайте определим следующий специальный класс исключений SomeException, который содержит некоторую необязательную контекстную информацию и функцию-член getContext для ее получения:

class SomeException : public std::runtime_error
{
    std::string mContext;

public:
    SomeException(
        const std::string& message,
        const std::string& context)
        : std::runtime_error(message), mContext(context)
    {}

    const std::string& getContext() const noexcept{
        return mContext;
    }
};

Добавив новый аргумент context к FooResouce::FooResouce и заменив throw на THROW_WITH_NESTED, мы можем передать первое требование в рамках вышеуказанной структуры обработки ошибок:

class FooResouce
{
public:
    FooResouce(
        T arg_to_construct_with,
        const std::string& context)
    {
        m_foo = create_foo_resouce(arg_to_construct_with);
        if(!m_foo){
            THROW_WITH_NESTED(SomeException, "Ctor failed.", context);
        }
        ...
    }
    ...
};

Далее,

но у нас нет информации о том, где исключение было вызвано на верхнем уровне.Здесь верхний уровень относится к функции, которая пыталась создать FooResource,

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

CATCH_BEGIN // most outer try-catch block in each thread
...

auto resource = HOOK(FooResouce(T(), "context"));
...

CATCH_END(std::cout)

Наконец,

В месте вызова используйте функцию pushContext.Эта функция будет хранить контекст, используя локальное хранилище потока.

Хотя я не знаю деталей этого требования, но мы можем вызвать SomeException::getContext в output_exceptions_impl следующим образоми получить все контексты из каждого брошенного SomethingExceptions, я думаю, мы также можем хранить их следующим образом:

DEMO (мое предложение)

void output_exceptions_impl(
    const std::exception& exception,
    std::ostream& stream,
    bool isFirstCall = false)
{
    ...
    catch (const SomeException& e) { // This section is added.
        stream 
            << "SomeException error: context:" 
            << e.getContext() << ", "; // or pushContext?
        output_exceptions_impl(e, stream);
    }
    ...
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...