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

时间:2012-11-13 07:53:35

标签: c++

我想在C ++中实现一个Exception类,它模仿.NET框架中的一个(并且Java也有类似的东西),用于以下目的:

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

  2. 异常继承:例如,应该可以从IoException派生Exception,从SerialPortException派生IoException。虽然这看起来微不足道,但应该有能力动态识别捕获的异常类型(例如,用于记录目的,或显示给用户),最好没有RTTI和typeid的开销。

  3. 这是我想要实现的示例异常处理逻辑:

    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成员解析和格式化整个异常链,以显示如下内容:

    Exception chain formatting

    到目前为止,我已经提出了以下实现:

    小注意:CString是特定于Microsoft的字符串类(仅适用于不熟悉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 ++中这个概念的最佳实现?或者也许我这样做完全错了?

    PS:我知道C ++ 11(也在Boost中)存在一些机制用于此目的(异常链接)和一些新的异常类,但我主要对自定义感兴趣,“old-C ++兼容”方法。但是,如果有人能够在C ++ 11中提供任何完成同样的代码,那就更好了。

4 个答案:

答案 0 :(得分:16)

C ++ 11已经有nested_exception。在Boostcon / C ++ 2012年的C ++ 03和C ++ 11中有一个关于例外的讨论。视频在youtube上:

  1. http://www.youtube.com/watch?v=N9bR0ztmmEQ&feature=plcp
  2. http://www.youtube.com/watch?v=UiZfODgB-Oc&feature=plcp

答案 1 :(得分: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);

答案 2 :(得分: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

答案 3 :(得分:1)

几年前我写了这个:Unchaining Chained Exceptions in C++

基本上,异常并不是彼此嵌套的,因为它很难捕获原始异常,但是另一种机制会跟踪异常在访问其捕获点时所访问的所有函数。

可以在Bitbucket上的Imebra库herehere中找到它的重访版本。

现在我会通过一些改进重写(例如使用本地线程存储来保持堆栈跟踪)。

使用这种方法可以捕获抛出的原始异常,但仍然具有堆栈跟踪以及异常访问的函数在返回catch语句时可能添加的其他信息。