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
Re: best practices по поводу исключений в C++
От: Alexander G Украина  
Дата: 24.12.13 09:33
Оценка:
boost::exception — хорош для диагностики, типа __FILE__, __LINE__.
Через BOOST_THROW_EXCEPTION можно добавить этот сахар, не вторгаясь в иерархию исключений (и при этом можно легко выбросить его в релизе, если попадание в бинарник лишней информации нежелательно).

для добавления информации о различных исключительных ситуациях можно взять boost::system::system_error с boost::system::error_code.
он же std::system_error c std::error_code.

идея там следующая: есть коды возврата, типа int или enum, туда попадают всякие существующие коды ошибок, типа HRESULT, свои вводятся через enum.
есть категории, так что разные виды кодов ошибок различаются по этим категориям, также категории отвечают за преобразование такого кода в строку.

в этом случае для своей библиотеки можно создать свою error category, тогда добавления новой исключительной ситуации — расширяется свой enum.
Русский военный корабль идёт ко дну!
Re[2]: best practices по поводу исключений в C++
От: flаt  
Дата: 24.12.13 09:59
Оценка: 6 (1)
Здравствуйте, Alexander G, Вы писали:

AG>идея там следующая: есть коды возврата, типа int или enum, туда попадают всякие существующие коды ошибок, типа HRESULT, свои вводятся через enum.

AG>есть категории, так что разные виды кодов ошибок различаются по этим категориям, также категории отвечают за преобразование такого кода в строку.
Плюс можно сделать сахар для дилеммы "исключения vs код возврата":

void sometimes_throws(int arg, std::error_code& ec = throws());

int main() {
  // throw if failed:
  try {
    sometimes_throws(0);
  } catch(std::system_error& e) {
      std::cerr << e.code();
  }

  // do not throw:
  std::error_code ec;
  sometimes_throws(0, ec);
  std::cerr << ec;
}
Re: best practices по поводу исключений в C++
От: Кодт Россия  
Дата: 24.12.13 10:11
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

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


FH>До недавнего времени во всех своих проектах я поступал следующим образом:

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

FH>
FH>#include <boost/config.hpp>

FH>#include <exception>
FH>#include <string>

FH>namespace http {

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

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

FH>private:
FH>  const std::string _msg;
FH>};

FH>// ...

FH>} // namespace http
FH>


FH>Минусы такого подхода следующие:

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

Для этого нам даны шаблоны

(Сам я не монстр создания иерархий исключений, поэтому лишь поделюсь мыслями)
// создание плоской иерархии
template<class Tag> class tagged_exception : public std::exception { ..... };

struct foo_ex_tag {};
struct bar_ex_tag {};

try { ... throw tagged_exception<foo_ex_tag>("oh deer"); ... }
catch(tagged_exception<foo_ex_tag> const& e) { ... e.what() ... }

// миксины
template<class MixIn, class CtorArg = MixIn> class mixed_exception : public std::exception, public MixIn { ..... };

struct foo_mixin { /* дополнительные поля */ };

try { ... throw mixed_exception<foo_mixin>( foo_mixin(1,2,3) ); ... }
catch(mixed_exception<foo_mixin> const& e) { ... e.foo_member ... };

// CRTP

template<class Final> class curious_exception : public std::exception { .... static_cast<Final*>(this) ..... };

struct foo_exception : curious_exception<foo_exception> { ..... };

try { ... throw foo_exception("let eat bee", 1,2,3); ... };
catch(foo_exception const& e) { ... e.foo_member ... };


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


Идеология исключений такая: try должен выполняться почти бесплатно, на throw можно и раскошелиться, а каскад catch — дёшево.
Поэтому само исключение можно делать тяжёлым (и кидать его как можно реже), но сопоставление лучше делать именно на уровне типов, а не
catch(e){ if(!we_can_handle(e)) throw; } ... }


Код, обменивающийся исключениями, как нуклоны — виртуальными частицами, — это смерть производительности.
Перекуём баги на фичи!
Re: best practices по поводу исключений в C++
От: niXman Ниоткуда https://github.com/niXman
Дата: 24.12.13 10:14
Оценка: 7 (1)
Здравствуйте, FrozenHeart, Вы писали:

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

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

FH>private:
FH>  const std::string _msg; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
FH>};

всегда поражался такой практике, выделать память при выбросе исключений =)
искать последствия такой ситуации будет мучительно больно.

зы
#define STR_(t) #t
#define STR(t) Str_(t)
#define MY_THROW(type, msg) \
   throw type(__FILE__ "(" STR(__LINE__) "): " msg)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[2]: best practices по поводу исключений в C++
От: niXman Ниоткуда https://github.com/niXman
Дата: 24.12.13 10:16
Оценка:
комент не туда вписал %)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[2]: best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 10:26
Оценка:
AG> Через BOOST_THROW_EXCEPTION можно добавить этот сахар, не вторгаясь в иерархию исключений (и при этом можно легко выбросить его в релизе, если попадание в бинарник лишней информации нежелательно).

Да, там это уже есть, однако:
— Под капотом он использует __FILE__, который, в зависимости от того или иного компилятора, может раскрыться как в полный путь к файлу, так и отдельно само название файла. Мне, например, нужно лишь второе (наблюдать огромный путь в логах не самое приятное занятие)
— Макрос BOOST_THROW_EXCEPTION не добавляет к выбрасываемому исключению call stack

AG> для добавления информации о различных исключительных ситуациях можно взять boost::system::system_error с boost::system::error_code.

AG> он же std::system_error c std::error_code.

Так зачем, если удобнее сделать это в виде исключений с получением полного стек трейса и подробным описанием возникшей ошибки? Комбинировать std::system_error с исключениями, честно говоря, не вижу смысла. Может, я чего-то не понимаю?
avalon/1.0.433
Re[2]: best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 10:31
Оценка:
X> всегда поражался такой практике, выделать память при выбросе исключений =)
X> искать последствия такой ситуации будет мучительно больно.

А как иначе? Где-то же эту информацию хранить всё же надо. Ну, не в виде члена класса (я, собственно, об этом и писал, в том числе), а в каком-то другом месте.

X> зы

X>
X> #define STR_(t) #t
X> #define STR(t) Str_(t)
X> #define MY_THROW(type, msg) \
X>    throw type(__FILE__ "(" STR(__LINE__) "): " msg)
X>


Можно и так, вот только пользователь той или иной подсистемы может захотеть получить лишь описание ошибки, без всякой дополнительной ерунды, которая ему в данном случае никак не поможет. Более того, вот захочу я добавить стек трейс. Что я должен буду сделать? Добавить его в виде текстового представления всё в ту же строку? В каком именно формате? Всегда? Не лучше ли дать решить это самому пользователю?
avalon/1.0.433
Re[3]: best practices по поводу исключений в C++
От: niXman Ниоткуда https://github.com/niXman
Дата: 24.12.13 10:32
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>- Под капотом он использует __FILE__, который, в зависимости от того или иного компилятора, может раскрыться как в полный путь к файлу, так и отдельно само название файла. Мне, например, нужно лишь второе (наблюдать огромный путь в логах не самое приятное занятие)

это можно обработать в компайл-тайме.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[3]: best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 10:32
Оценка:
f> Плюс можно сделать сахар для дилеммы "исключения vs код возврата":

А зачем может понадобиться комбинировать исключения с кодами ошибок? Не лучше ли на этом этапе вместо числа получить подробный текст, описывающий, что же на самом деле произошло?
avalon/1.0.433
Re[3]: best practices по поводу исключений в C++
От: niXman Ниоткуда https://github.com/niXman
Дата: 24.12.13 10:34
Оценка: -1
Здравствуйте, FrozenHeart, Вы писали:

FH>Можно и так, вот только пользователь той или иной подсистемы может захотеть получить лишь описание ошибки, без всякой дополнительной ерунды, которая ему в данном случае никак не поможет. Более того, вот захочу я добавить стек трейс. Что я должен буду сделать? Добавить его в виде текстового представления всё в ту же строку? В каком именно формате? Всегда? Не лучше ли дать решить это самому пользователю?


эта удобность может обойтись слишком дорого.
имени файла и строки, достаточно для выявления ошибки в 99%.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[4]: best practices по поводу исключений в C++
От: niXman Ниоткуда https://github.com/niXman
Дата: 24.12.13 10:38
Оценка: +1
я к такому правилу пришел постепенно. сначала я отказался от выделения памяти при выбросе исключения, путем использования предаллоцирования пула строк. это уже было лучше. потом наступила ситуация, когда даже этот пул строк все усложнял — отказался и от него.
сейчас не жалуюсь ни на что.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[4]: best practices по поводу исключений в C++
От: flаt  
Дата: 24.12.13 11:16
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

f>> Плюс можно сделать сахар для дилеммы "исключения vs код возврата":


FH>А зачем может понадобиться комбинировать исключения с кодами ошибок?

Это не комбинация. Автор библиотеки (автор функции) отдаёт политику разруливания ошибок на откуп её пользователям — хотят, используют исключения, не хотят — у них есть код ошибки. По ссылке подробности.
Re[4]: best practices по поводу исключений в C++
От: PPA Россия http://flylinkdc.blogspot.com/
Дата: 24.12.13 12:20
Оценка:
Здравствуйте, niXman, Вы писали:

X>эта удобность может обойтись слишком дорого.

X>имени файла и строки, достаточно для выявления ошибки в 99%.

Т.е. исключение в недрах функции, которая зовется из разных мест это такая редкость?
или я не понял как без стека узнать кто позвал метод...
Re[5]: best practices по поводу исключений в C++
От: niXman Ниоткуда https://github.com/niXman
Дата: 24.12.13 12:43
Оценка: -1
Здравствуйте, PPA, Вы писали:

PPA>Т.е. исключение в недрах функции, которая зовется из разных мест это такая редкость?

да нет. большинство функций зовутся из разных мест

PPA>или я не понял как без стека узнать кто позвал метод...

эта информация не первостепенной важности.
зная где исключение было выброшено, позволяет сузить понимание причины его возникновения.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[3]: best practices по поводу исключений в C++
От: McQwerty Россия  
Дата: 24.12.13 13:53
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

X>>
X>> #define STR_(t) #t
X>> #define STR(t) Str_(t)
X>> #define MY_THROW(type, msg) \
X>>    throw type(__FILE__ "(" STR(__LINE__) "): " msg)
X>>


Если это в динамической библиотеке, которая может быть выгружена — недалеко до беды с этим указателем.
Re[5]: best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 19:22
Оценка:
f> Это не комбинация. Автор библиотеки (автор функции) отдаёт политику разруливания ошибок на откуп её пользователям — хотят, используют исключения, не хотят — у них есть код ошибки. По ссылке подробности.

А почему можно отказаться от работы с исключениями и решить использовать коды ошибок? Запрещены по code style'у / особенностям платформы? Из-за проблем с производительностью, которые явно показал профилировщик?

Стоит ли вообще над этим заморачиваться? Мне просто кажется, что лучше уж меньше свободы юзерам дать в этом плане, зато чётко указать своё отношение по данному вопросу в документации, чем иметь в 90% случаев лишний код для поддержки кодов ошибок.
avalon/1.0.433
Re[4]: best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 19:22
Оценка:
X> это можно обработать в компайл-тайме.

Ну, можно. Вот только в случае BOOST_THROW_EXCEPTION как Вы это сделаете (я же именно про него в том сообщении говорил)? Переопределите макрос __FILE__ перед включением соответствующего заголовочного файла boost'а?
avalon/1.0.433
Re[5]: best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 19:22
Оценка:
X> я к такому правилу пришел постепенно. сначала я отказался от выделения памяти при выбросе исключения, путем использования предаллоцирования пула строк. это уже было лучше. потом наступила ситуация, когда даже этот пул строк все усложнял — отказался и от него.
X> сейчас не жалуюсь ни на что.

А какие проблемы-то возникали? Нехватка памяти (раз Вы заговорил про предаллоцированный пул строк)? Я просто не о подобного рода ошибках говорю.
avalon/1.0.433
Re[6]: best practices по поводу исключений в C++
От: FrozenHeart  
Дата: 24.12.13 19:22
Оценка:
X> эта информация не первостепенной важности.
X> зная где исключение было выброшено, позволяет сузить понимание причины его возникновения.

Сузить, но не сократить до минимума. Хотя, всё зависит от проекта.
avalon/1.0.433
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.