考虑以下资源管理类
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;
};
现在,当我们决定捕获异常并格式化错误消息时,很容易从根本上知道是什么原因导致了该异常,但是我们没有关于 where 的信息。 em>异常是在上级中触发的。这里的上层是指试图创建FooResource的函数,或者是该堆栈框架上方的任何函数。在需要的情况下,如何为错误添加上下文:
pushContext
函数。此功能将使用线程本地存储来存储上下文。答案 0 :(得分:2)
尽管此解决方案与您的第三个要求 3:不捕获并重新投放相冲突,但我还是提出了一个std::nested_exception
和宏的解决方案,因为这似乎为当前的问题提供了合理的解决方案至少对我来说。
我希望这个答案过长可以为您提供帮助。
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
来避免出现this帖子中指出的异常:
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帖子中建议的以下宏:
#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)
然后,我们得到以下输出,其中文件名和行号仅是示例。 记录所有可用信息:
运行时错误:prog.cc,l.119,h(),更高级别。
运行时错误:prog.cc,l.113,g(),更高级别。
运行时错误:prog.cc,l.109,f(),更高级别。
逻辑错误:prog.cc,l.105,SomeException,基本级别。
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
中获取所有上下文,所以我认为我们可以也像这样存储它们:
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);
}
...
}