17

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

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

  2. 异常继承:例如应该可以IoExceptionExceptionSerialPortException从派生IoException。虽然这看起来微不足道,但应该能够动态识别捕获的异常类型(例如,用于记录目的或显示给用户),最好没有 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是 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 中)为此目的(异常链接)存在一些机制,并带有一些新的异常类,但我主要对自定义的“旧 C++ 兼容”方式感兴趣。但是,另外,如果有人可以提供任何 C++11 中的代码来完成同样的事情,那就太好了。

4

4 回答 4

19

C++11 已经有了nested_exception。在 Boostcon/C++Next 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
于 2012-11-13T08:04:22.523 回答
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);
于 2012-11-13T08:05:23.560 回答
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

于 2014-02-03T06:32:03.073 回答
2

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

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

可以在 Bitbucket 上的 Imebra 库中找到它的重新访问版本,这里这里

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

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

于 2012-11-15T08:10:24.013 回答