Здравствуйте, коллеги.
Задаюсь уже не первый день следующим вопросом — как организовать наиболее опрятную, грамотную, универсальную и удобную в использовании систему исключений в 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-блоке не даст того же результата?