best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 09:03
Оценка:
Здравствуйте, коллеги.

Задаюсь уже не первый день следующим вопросом — как организовать наиболее опрятную, грамотную, универсальную и удобную в использовании систему исключений в C++?

До недавнего времени во всех своих проектах я поступал следующим образом:
— Придерживался правила "Всегда выбрасывать исключения, если для сигнализации ошибки функции требуется больше, чем переменная типа bool в виде возвращаемого значения". Просадок в производительности в нашей предметной области это не вызывало, так что такой подход никакого заметного оверхеда не вносил.
— Имея отдельную подсистему, занимающуюся только своей задачей (например, подсистема для работы с HTTP), я заводил для неё как минимум один собственный класс исключений, наследуемый от std::exception, даже в том случае, если я не планирую добавлять в него никакого функционала в виде дополнительных функций-членов, кроме переопределённого what.

#include <boost/config.hpp>

#include <exception>
#include <string>

namespace http {

class exception : public std::exception
{
public:
  exception(const std::string& msg) : _msg(msg) {}

  /* virtual */ const char* what() const override BOOST_NOEXCEPT
  {
    return _msg.c_str();
  }

private:
  const std::string _msg;
};

// ...

} // namespace http


Минусы такого подхода следующие:
— Каждый раз при объявлении нового типа исключений приходится заниматься стандартными вещами наподобие определением конструктора, переопределением what, etc, даже несмотря на то, что их реализация всегда одна и та же. Можно, конечно, вынести код с переопределением функции-члена what в базовый класс, но неудобство с определением конструктора всё равно остаётся
— При добавлении нового функционала в класс исключения придётся добавлять сеттеры / геттеры, соответствующие члены класса, etc. Помимо того, что это не совсем элегантно, мы таким образом делаем класс исключения более "тяжеловесным", что, насколько я понимаю, немного противоречит идеологии их использования

Эти и некоторые другие минусы призвана решить часть библиотеки boost под названием boost exception. Вот, что у меня получилось в итоге:

#define MY_THROW(Ex) \
  throw Ex << base_exception::location_info(debug::location(__FILE__, BOOST_CURRENT_FUNCTION, __LINE__)) \
           << base_exception::trace_info(debug::get_call_stack())

class base_exception
  : virtual public std::exception
  , virtual public boost::exception
{
public:
  typedef boost::error_info<struct tag_backtrace, std::vector<std::string>> trace_info;
  typedef boost::error_info<struct tag_location, debug::location> location_info;
  typedef boost::error_info<struct tag_reason, std::string> reason_info;

  static reason_info reason(const std::string& str)
  {
    return reason_info(str);
  }

  template <typename T>
  const typename T::value_type* get() const
  {
    return boost::get_error_info<T>(*this);
  }

  const std::vector<std::string>& call_stack() const
  {
    if (const std::vector<std::string>* result = get<trace_info>())
    {
      return *result;
    }
    return std::vector<std::string>();
  }

  virtual std::string reason() const
  {
    if (const std::string* result = get<reason_info>())
    {
      return *result;
    }
    return std::string();
  }

  const debug::location& where() const
  {
    if (const debug::location* result = get<location_info>())
    {
      return *result;
    }
    return debug::location();
  }

  virtual const char* what() const throw() override
  {
    return boost::diagnostic_information_what(*this);
  }
};

class some_exception : public base_exception {};


Выбрасывание исключений в итоге выглядит следующим образом:

MY_THROW(some_exception())
      << base_exception::reason("Some exception");


Следовательно, на принимающей стороне, помимо описания причины возникновения ошибки, я всегда могу получить call stack и место, откуда оно было выброшено.

Правила в отношении исключений выглядят следующим образом:
— Каждый тип исключения наследуется от base_exception, который, в свою очередь, является наследником классов std::exception и boost::exception
— Благодаря тому, что базовый класс исключений наследуется от boost::exception, при помощи operator<< в него можно добавлять дополнительную информацию, которая может понадобиться пользовательскому коду
— Исключения должны (до тех пор, пока явно не понадобится обратного) выбрасываться при помощи макроса MY_THROW, который добавляет к выбрасываемому исключению call stack и место, откуда оно было выброшено

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

#define MY_THROW(Ex) \
  throw Ex << base_exception::location_info(debug::location(__FILE__, BOOST_CURRENT_FUNCTION, __LINE__)) \
           << base_exception::original_type_info(typeid((Ex)).name()) \
           << base_exception::trace_info(debug::get_call_stack())


Для чего это нужно? Разве typeid в catch-блоке не даст того же результата?
avalon/1.0.433
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.