Хотя это решение противоречит вашему третьему требованию 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
Первое требование:
- Передать некоторую необязательную контекстную информацию в качестве дополнительного аргумента 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);
}
...
}