Подтвердить что ты не робот

Правильный/элегантный способ реализации цепочки исключений С++?

Я хотел бы реализовать класс Exception в С++, который имитирует один из .NET framework (и Java тоже имеет нечто подобное) для следующих целей:

  • Цепочка исключений: я хотел бы реализовать концепцию "трансляции исключений", когда исключения, захваченные на более высоких уровнях, обертывают и "переводят" исключения нижнего уровня, также как-то сохраняя эти исключения уровня любовника (в InnerException, в этом случае). Для этого должен быть некоторый механизм для хранения внутренних исключений вместе с каждым исключением, созданным на верхнем уровне. InnerException член предоставляет это в реализации ниже.

  • Наследование исключения: должно быть возможно получить IoException из Exception и SerialPortException из 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, чтобы отобразить что-то вроде этого:

Exception chain formatting

Я до сих пор придумал следующую реализацию:

Небольшое примечание: CString - это строковый класс, специфичный для Microsoft (только для людей, не знакомых с материалами Visual С++).

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...), просто... Я не думаю, что это изящное решение. Но я не мог придумать лучшего. Есть ли у вас идеи, как реализовать эту концепцию "правильно"? Или, может быть, это лучшая реализация этой концепции, которая возможна в С++? Или, может быть, я делаю это совершенно неправильно?

PS: Я знаю, что для этой цели существуют некоторые механизмы в С++ 11 (также в Boost) (цепочка исключений) с некоторыми новыми классами исключений, но меня в первую очередь интересует пользовательский "old-С++-compatible", пути. Но было бы хорошо, кроме того, если бы кто-то мог предоставить какой-либо код на С++ 11, который выполнит то же самое.

4b9b3361

Ответ 2

Существует много дополнительного кода, но хорошо, что это действительно EASY дополнительный код, который вообще не изменяется от класса к классу, поэтому он может препроцессорным макросом.

#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);

Ответ 3

Пожалуйста, не следует использовать метод 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

Как вы видите, есть исключения, связанные друг с другом, а диагностический вывод содержит все исключения с информацией о контексте и дополнительной трассировкой стека (здесь не показано, потому что это зависит от компилятора/платформы). "Цепочка исключений" может быть достигнута, естественно, с использованием новых функций обработки ошибок С++ 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)

Я использую компилятор с частичной поддержкой С++ 11 (gcc 4.4.7), поэтому здесь вы можете увидеть некоторые старые стили кода. Для справки вы можете использовать следующие параметры компиляции для построения этого примера (-rdynamic для трассировки стека):

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

Ответ 4

Несколько лет назад я написал следующее: Unchaining Chained Exceptions в С++

В принципе, исключения не вложены внутри друг друга, потому что было бы сложно поймать исходный, но другой механизм отслеживает все функции, которые посетил исключение, пока он перемещается в точку его улова.

Пересмотренная версия этого может быть найдена в библиотеке Imebra on Bitbucket, здесь и .

Теперь я должен переписать это с некоторыми улучшениями (например, использовать хранилище локальных потоков для сохранения трассировки стека).

Используя этот подход, вы можете поймать исходное исключение, которое было выбрано, но все еще иметь трассировку стека и, возможно, другую информацию, добавленную функциями, которые были посещены этим исключением, в то время как она возвращается к выписке.