从异常中恢复上下文



考虑以下资源管理类

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. 捕获并重新抛出。我认为在这种情况下,代码会很难看

尽管此解决方案与您的第三个需求相冲突3:No Catch and rethrow,但我建议使用std::nested_exception和宏的解决方案,因为至少对我来说,这似乎为当前问题提供了一个合理的解决方案。我希望这个太长的答案能帮助你。


1.std::nested_exception的错误处理


首先,我们可以使用std::nested_exception递归嵌套异常。粗略地说,我们可以通过调用std::throw_with_nested来向这个类添加任意类型的异常。这使我们能够用一个相当简单的代码携带抛出异常的所有信息,只需通过上层中的每个省略号捕获处理程序catch(…){ }中的std::throw_with_nested抛出每个异常。

例如,下面的函数h抛出一个std::nested_exception,它聚合了用户定义的异常SomeExceptionstd::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."));
}    
}

定位异常(基本级别)

通过宏THROW_WITH_NESTED,用下面的函数throw_with_nested_wrapper替换所有这些std::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)...));
};

定位异常(上层)

如果我们必须获取有关在上层中触发异常的位置的信息,那么以下重用上述宏THROW_WITH_NESTED的宏HOOK将适用于我们:

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

最后,前三个函数fgh被重写并简化如下:

[[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来避免它,正如thispost:中所指出的那样

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_NESTEDHOOKCATCH_BEGINCATCH_END,我们可以在每个线程中定位异常:

CATCH_BEGIN // most outer try-catch block in each thread
...
HOOK(h());
...
CATCH_END(std::cout)

然后我们得到以下输出,其中文件名和行号只是一个例子。记录所有可用信息:

带2个线程的DEMO

运行时错误: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;
}
};

FooResouce::FooResouce中添加一个新的参数context,并用THROW_WITH_NESTED替换throw,我们可以通过上述错误处理框架中的第一个要求:

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的函数,

使用HOOK创建每个FooResource,我们可以获得有关ctor在上层发生故障的信息。主叫方如下。通过这种方式,包括消息、上下文及其位置在内的所有错误信息都将在每个线程中得到澄清。

CATCH_BEGIN // most outer try-catch block in each thread
...
auto resource = HOOK(FooResouce(T(), "context"));
...
CATCH_END(std::cout)

最后,

  1. 在调用站点使用pushContext函数。此函数将使用线程本地存储来存储上下文

虽然我不知道这个需求的细节,但由于我们可以如下调用output_exceptions_impl中的SomeException::getContext,并从每个抛出的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);
}
...
}

最新更新