实现C++异常链接的正确/优雅方式是什么?

17

我希望在C++中实现一个异常类,模仿.NET框架中的异常类(Java也有类似的实现),以实现以下目的:

  1. 异常链:我想要实现“异常转换”的概念,当在更高层次捕获异常并包装和“翻译”较低级别的异常时,还要以某种方式保留这些较低级别的异常(在这种情况下,在InnerException成员中)。为此,应该有一些机制来存储每个在上层抛出的异常的内部异常。在下面的实现中,InnerException成员提供了这个功能。

  2. 异常继承:应该可以从Exception派生IoExceptionSerialPortException之类的异常。虽然这似乎很简单,但应该能够动态地识别捕获的异常类型(例如,为记录日志或显示给用户),最好不需要RTTI和typeid的开销。

以下是我想要实现的示例异常处理逻辑:

try
{
    try
    {
        try
        {
            throw ThirdException(L"this should be ThirdException");
        }
        catch(Exception &ex)
        {
            throw SubException(L"this should be SubException", ex);
        }
    }
    catch(Exception &ex)
    {
        throw SubException(L"this should be SubException again", ex);
    }
}
catch(Exception &ex)
{
    throw Exception(L"and this should be Exception", ex);
}

当在最上层捕获“最外层”异常时,我希望能够通过InnerException成员解析并格式化整个异常链,以显示类似于以下内容的内容:

异常链格式化

到目前为止,我想出了以下实现:

小注释:CString是微软特有的字符串类(只针对不熟悉Visual C++的人)。

class Exception
{
protected:

    Exception(const Exception&) {};
    Exception& operator= (const Exception&) {};

public:

    Exception(const CString &message) : InnerException(0), Message(message) {}
    Exception(const CString &message, const Exception &innerException) : InnerException(innerException.Clone()), Message(message) {}

    virtual CString GetExceptionName() const { return L"Exception"; }

    virtual Exception *Clone() const
    {
        Exception *ex = new Exception(this->Message);
        ex->InnerException = this->InnerException ? this->InnerException->Clone() : 0;
        return ex;
    }

public:

    virtual ~Exception() { if (InnerException) delete InnerException; }

    CString Message;
    const Exception *InnerException;
};

现在我们来看看这里有什么。拷贝构造函数和赋值操作符被设置为protected,以防止复制。每个对象将“拥有”其内部异常对象(并在析构函数中将其删除),因此默认的浅复制是不可接受的。然后我们有两个外观相当标准的构造函数和虚析构函数,用于删除InnerException对象。Clone()虚方法负责深度复制对象,主要用于存储内部异常对象(参见第二个构造函数)。最后,GetExceptionName()虚方法提供了一种廉价的用于识别异常类名的替代RTTI的方法(我不认为这看起来很酷,但我想不出更好的解决方案;例如:在.NET中,可以简单地使用someException.GetType().Name)。

现在这做到了它的工作。但是...我不喜欢这个解决方案的一个特定原因:每个派生类需要编写的代码量太多了。考虑一下我必须派生SubException类,它对基类功能完全没有任何增加,只是提供自定义名称(“SubException”,可能是“IoException”,“ProjectException”等等)以区分它的使用场景。我必须为每个这样的异常类提供几乎相同数量的代码。这里是:

class SubException : public Exception
{
protected:

    SubException(const SubException& source) : Exception(source) {};
    SubException& operator= (const SubException&) {};

public:

    SubException(const CString &message) : Exception(message) {};
    SubException(const CString &message, const Exception &innerException) : Exception(message, innerException) {};

    virtual CString GetExceptionName() const { return L"SubException"; }

    virtual Exception *Clone() const
    {
        SubException *ex = new SubException(this->Message);
        ex->InnerException = this->InnerException ? this->InnerException->Clone() : 0;
        return ex;
    }
};

我不喜欢每次都需要提供protected拷贝构造函数和赋值运算符,也不喜欢每次都需要复制Clone方法的代码,甚至还要复制基类成员的代码(如InnerException...),总之...我认为这不是优雅的解决方案。但我无法想到更好的方法。您有任何想法如何“正确”实现此概念吗?或者也许这是在C++中可能的最佳实现方式?或者我完全错了吗?

P.S.:我知道在C++11中存在一些机制(也在Boost中)用于此目的(异常链接)使用一些新的异常类,但我主要感兴趣的是自定义,“旧C++兼容”的方法。同时,如果有人能提供任何在C++11中完成相同工作的代码,那将是很好的。


5
这听起来像是 Boost.Exception。 - Pubby
也许Boost可以实现,但是我不能在这个项目中使用Boost。正如我所提到的,这可能直接在C++11中实现,但我正在寻找一种自定义的替代方案,不依赖于Boost、C++11或任何其他东西... - TX_
1
你可以使用Boost,或者至少从中提取一些类似这样的头文件。Boost有一些复杂的组件,但像异常处理这样的实用模块可以很容易地从中复制出来并包含在你的项目中。大部分的Boost,包括异常处理,都可以在Visual C++下编译通过,甚至包括旧的WinCE目标,如Pocket PC 2003(ARMV4)。 - Jan Hudec
请参见 https://dev59.com/mHA65IYBdhLWcg3w9DmF - Raedwald
4个回答

20

4

有很多额外的代码,但好的一点是这些代码非常简单,从一个类到另一个类都不会改变,因此可以使用预处理器宏来处理。

#define SUB_EXCEPTION(ClassName, BaseName) \
  class ClassName : public BaseName\
  {\
  protected:\
  \
      ClassName(const ClassName& source) : BaseName(source) {};\
      ClassName& operator= (const ClassName&) {};\
  \
  public:\
  \
      ClassName(const CString &message) : BaseName(message) {};\
      ClassName(const CString &message, const BaseName &innerException) : BaseName(message, innerException) {};\
  \
      virtual CString GetExceptionName() const { return L"ClassName"; }\
  \
      virtual BaseName *Clone() const\
      {\
          ClassName *ex = new ClassName(this->Message);\
          ex->InnerException = this->InnerException ? this->InnerException->Clone() : 0;\
          return ex;\
      }\
  };

接下来,您只需执行以下操作即可定义各种实用程序异常:

SUB_EXCEPTION(IoException, Exception);
SUB_EXCEPTION(SerialPortException, IoException);

@TX_ Microsoft 和 Boost 在宏的使用方面相当广泛 - 你应该看看 Boost Fusion 来体验一下宏编程的乐趣 ;) http://www.boost.org/doc/libs/1_52_0/libs/fusion/doc/html/index.html - Caribou

2
请不要使用boost::exception方法。Boost::exception是用于不同的用例,特别是当您想收集分散在调用堆栈上的精确异常上下文时它非常有用。考虑以下示例:
#include "TSTException.hpp"

struct DerivedException: TST::Exception {};

int main() try
{
    try
    {
        try
        {
            try
            {
                throw std::runtime_error("initial exception");
            }
            catch(...)
            {
                throw TST::Exception("chaining without context info");
            }
        }
        catch(...)
        {
            TST_THROW("hello world" << '!');
        }
    }
    catch(...)
    {
        TST_THROW_EX(DerivedException, "another exception");
    }
}
catch(const TST::Exception& ex)
{
    cout << "diagnostics():\n" << ex;
}
catch(const std::exception& ex)
{
    cout << "what(): " << ex.what() << endl;
}

据我理解,“异常链”解决方案应该会产生类似于以下输出结果的内容:
$ ./test
diagnostics():
Exception: another exception raised from [function: int main() at main.cpp:220]
Exception: hello world! raised from [function: int main() at main.cpp:215]
Exception: chaining without context info raised from [function: unknown_function at unknown_file:0]
Exception: initial exception

正如您所看到的,有一些异常链接在一起,诊断输出包含所有带有上下文信息和可选堆栈跟踪的异常(这里未显示,因为它取决于编译器/平台)。 使用新的C++11错误处理功能(std::current_exception或std::nested_exception),可以自然地实现“异常链接”。这是TSTException.hpp的实现(请耐心阅读更多源代码):

#include <iostream>
#include <sstream>
#include <stdexcept>
#include <exception>
#include <vector>
#include <string>
#include <memory>
#include <boost/current_function.hpp>
#include <boost/foreach.hpp>

using namespace std;

namespace TST
{

class Exception: virtual public std::exception
{
public:
    class Context
    {
    public:
        Context():
            file_("unknown_file"),
            line_(0),
            function_("unknown_function")
        {}
        Context(const char* file, int line, const char* function):
            file_(file? file: "unknown_file"),
            line_(line),
            function_(function? function: "unknown_function")
        {}
        const char* file() const { return file_; }
        int line() const { return line_; }
        const char* function() const { return function_; }
    private:
        const char* file_;
        int line_;
        const char* function_;
    };
    typedef std::vector<std::string> Stacktrace;
    //...
    Exception()
    {
        initStacktraceAndNestedException();
    }
    explicit Exception(const std::string& message, const Context&& context = Context()):
        message_(message),
        context_(context)
    {
        message.c_str();
        initStacktraceAndNestedException();
    }
    ~Exception() throw() {}
    //...
    void setContext(const Context& context) { context_ = context; }
    void setMessage(const std::string& message) { (message_ = message).c_str(); }
    const char* what() const throw () { return message_.c_str(); }
    void diagnostics(std::ostream& os) const;
protected:
    const Context& context() const { return context_; }
    const std::exception_ptr& nested() const { return nested_; }
    const std::shared_ptr<Stacktrace>& stacktrace() const { return stacktrace_; }
    const std::string& message() const { return message_; }
private:
    void initStacktraceAndNestedException();
    void printStacktrace(std::ostream& os) const;
    std::string message_;
    Context context_;
    std::shared_ptr<Stacktrace> stacktrace_;
    std::exception_ptr nested_;
};

std::ostream& operator<<(std::ostream& os, const Exception& ex)
{
    ex.diagnostics(os);
    return os;
}

std::ostream& operator<<(std::ostream& os, const Exception::Context& context)
{
    return os << "[function: " << context.function()
              << " at " << context.file() << ':' << context.line() << ']';
}

void Exception::diagnostics(std::ostream& os) const
{
    os << "Exception: " << what() << " raised from " << context_ << '\n';
    if (const bool haveNestedException = nested_ != std::exception_ptr())
    {
        try
        {
            std::rethrow_exception(nested_);
        }
        catch(const TST::Exception& ex)
        {
            if(stacktrace_ && !ex.stacktrace())//if nested exception doesn't have stacktrace then we print what we have here
                    printStacktrace(os);
            os << ex;
        }
        catch(const std::exception& ex)
        {
            if(stacktrace_)
                printStacktrace(os);
            os << "Exception: " << ex.what() << '\n';
        }
        catch(...)
        {
            if(stacktrace_)
                printStacktrace(os);
            os << "Unknown exception\n";
        }
    }
    else if(stacktrace_)
    {
        printStacktrace(os);
    }
}

void Exception::printStacktrace(std::ostream& os) const
{
    if(!stacktrace_)
    {
        os << "No stack trace\n";
        return;
    }
    os << "Stack trace:";
    BOOST_FOREACH(const auto& frame, *stacktrace_)
    {
        os << '\n' << frame;
    }
    os << '\n';
}

void Exception::initStacktraceAndNestedException()
{
    nested_ = std::current_exception();
    if(const bool haveNestedException = nested_ != std::exception_ptr())
    {
        try
        {
            throw;
        }
        catch(const TST::Exception& ex)
        {
            if(ex.stacktrace())
            {
                stacktrace_ = ex.stacktrace();
                return;
            }
        }
        catch(...) {}
    }
    /*TODO: setStacktrace(...); */
}

}//namespace TST

#ifdef TST_THROW_EX_WITH_CONTEXT
#error "TST_THROW_EX_WITH_CONTEXT is already defined. Consider changing its name"
#endif /*TST_THROW_EX_WITH_CONTEXT*/

#define TST_THROW_EX_WITH_CONTEXT(                                      \
    CTX_FILE, CTX_LINE, CTX_FUNCTION, EXCEPTION, MESSAGE)               \
    do                                                                  \
    {                                                                   \
        EXCEPTION newEx;                                                \
        {                                                               \
            std::ostringstream strm;                                    \
            strm << MESSAGE;                                            \
            newEx.setMessage(strm.str());                               \
        }                                                               \
        newEx.setContext(                                               \
            TST::Exception::Context(                                    \
                CTX_FILE, CTX_LINE, CTX_FUNCTION));                     \
        throw newEx;                                                    \
    }                                                                   \
    while(0)

#ifdef TST_THROW_EX
#error "TST_THROW_EX is already defined. Consider changing its name"
#endif /*TST_THROW_EX*/

#define TST_THROW_EX(EXCEPTION, MESSAGE)                                       \
    TST_THROW_EX_WITH_CONTEXT(__FILE__, __LINE__, BOOST_CURRENT_FUNCTION, EXCEPTION, MESSAGE)

#ifdef TST_THROW
#error "TST_THROW is already defined. Consider changing its name"
#endif /*TST_THROW*/

#define TST_THROW(MESSAGE)                                              \
    TST_THROW_EX(TST::Exception, MESSAGE)

我使用带有部分C++11支持的编译器(gcc 4.4.7),因此您可能会在这里看到一些旧样式的代码。仅供参考,您可以使用以下编译参数构建此示例(-rdynamic用于堆栈跟踪):

g++ main.cpp TSTException.hpp -rdynamic -o test -std=c++0x


2
几年前我写了这篇文章:在C++中解除链式异常。基本上,异常并不是嵌套在彼此内部的,因为很难捕获原始异常,但另一种机制会跟踪所有由异常访问的函数,当它前往catch点时。
该库Imebra在Bitbucket上提供了重新审视的版本,在这里在这里
现在我想用一些改进来重写它(例如使用本地线程存储来保留堆栈跟踪)。
使用这种方法允许您捕获抛出的原始异常,但仍然具有堆栈跟踪和可能由异常访问的函数添加的其他信息,当其返回到catch语句时。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接