Об исключениях (C++)

Я не люблю C++-ные exceptions (за второй поток управления), как следствие - стараюсь их не использовать, а если использую, то перехватываю только те, которые порождает мой код. Как следствие, правил хорошего тона в этой области не знаю.

Возник вопрос, как правильно поступать. Вот есть такой примерно код:

int some_class::some_function(std::filebuf& buf)
 
  try {
      ....
      buf.sgetn(....);
      .....
      return 0; // OK
  }
  catch (my_own_exception_type t) {
        аккуратно_склеить_ласты();
        return errorcode;
   }
}
Вопрос: должен ли я в подобном коде ловить исключения, порожденные std::filebuf? Ну там не смог он ничего прочесть? А вообще все исключения? Как требуют понятия хорошего тона?

Должны ли быть эти правила хорошего тона разными в таких двух случаях

  • Этот самый std::filebuf - на самом деле хранится внутри класса, где-то раньше был создан/открыт и все такое. То есть это наш сукин сын.
  • Этот самый IO-хэндл (std::filebuf) передан нам снаружи т.е. это чужой сукин сын.
?

P.S. Нашелся йузер у которого для файлов с SD-читалки не работает std::filebuf IO. Linux, холст, масло....

Comments

Про все исключения - я у народа спрашивал: Вопрос: Как дела с catch(...) в MS? (и даже в корпоративной почте наставлял Стаса по поводу использования нашей библиотеки, кинул эту ссылку).

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

Если у тебя функция или метод не объявлен с спецификатором throws() - у тебя вообще никакой гарантии при вызове этого дела никогда быть не может.

Ага, цитата мне понроавилась:

за catch( ... ) без rethrow вообще надо убивать
или хотя бы гнать ссаными тряпками :)
потому что если где-то что-то сегфолтится и нейтрализуется этим кэтчем, то узнаешь ты об этом как правило слишком поздно

А как я могу (re)throw неизвестный мне тип исключения?

Цитирую последний стандарт.
15.1 Throwing an exception[except.throw]

A throw-expression with no operand rethrows the currently handled exception (15.3). The exception is
reactivated with the existing temporary; no new temporary exception object is created. The exception is
no longer considered to be caught; therefore, the value of std::uncaught_exception() will again be true.
[ Example: code that must be executed because of an exception yet cannot completely handle the exception
can be written like this:try {
// ...
} catch (...) { // catch all exceptions
// respond (partially) to exception
throw; // pass the exception to some
// other handler
} end example ]

В любом разе, это мою (чужую) проблему не лечит - она заключается именно в том, что исключение от std::filebuf никто не ловит.

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

Слови и обработай (там есть, о чём спросить у класса исключения) все потомки std::exception, например.

Ну у меня там всей обработки - аккуратно_склеить_ласты();

Но да, похоже, std::exception данную проблему лечит.

А где, кстати, почитать список исключений, порождаемых std:: ?

Есть стандарт. Есть Страуступ.
Как я описал выше, эта иерархия довольно консервативна...

Кроме того, с помощью нехитрых методов RTTI - ты можешь получить информацию о конкретном типе исключения (который на самом деле был пораждён), если ты поймал потомка std::exception, т.к. там задействована виртуальная таблица.

Я собираюсь об этом упомянуть на семинаре по CppUnit в офисе, если звёзды позволят нам этот dev-day провести...

Глядишь, много нового от про CMake узнаем... :-)

Я про CMake тут уже узнал достаточно (собирая llvm под виндами), чтобы бежать в ужасе....

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

Думаю, если Вы решили придерживаться подхода, когда функции не бросают исключения, а сообщают об ошибках (в том числе критичных) посредством возвращения кода, отличного от 0, то тогда будет вполне логичным последовательно придерживаться такого подхода. Т.е. эта вот функция "some_class::some_function" должна ловить все исключения и возвращать соответствующие коды ошибок. В конце концов, клиентов этой функции совершенно не волнует, какие другие функции она решит вызвать. Ну, не должно волновать. Инкапсуляция.

Вопрос в том, что мне делать с внешним миром. Ну вот в данном случае - совершенно какая-то нечеловеческая ошибка... я даже не знаю где. В-общем, Std::filebuf ломается при чтении первых двух байт файла, если этот файл - на SD-ридере.
Надо сказать, что ошибок файлового IO я как-то давно не видел в реальной жизни. Т.е. всякие коды возврата проверяю, но больше по привычке.

Ну ладно, этот std::filebuf я сам породил, сам, соответственно, обработаю (перехватив std::exception). Но ровно в тот же вызов юзер (девелопер) может передать свою реализацию LibRaw_abstract_datastream со своими исключениями. Тогда пусть сам и ловит, так что ли? Как-то это неровно получается.

Что делать "правильно"? Пусть юзер ловит, да. И карать всех, кто бросает исключения, не унаследованные от std::exception.

У Вас ведь сейчас метод возвращает error_code (0 в случае успеха). Юзер, очевидно, его анализирует и как то реагирует, да?

Зачем карать?

std::exception ловит моя библиотека. Если юзерский код кидается чем-то еще, то пусть сам и ловит, вроде так логично?

Ну так ведь это могут быть разные разработчики. Один, Вася, пишет "свою реализацию LibRaw_abstract_datastream", другой, Петя, ее использует, передавая в функцию/класс, которую написали Вы.

Вы, конечно, можете дистанцироваться от разработчика Васи и пусть Петя разбирается с Васей сам, если Вася бросит массив байт в качестве исключения. Но можно приложить небольшие усилия (если это возможно, конечно) к тому, чтобы все, что Вася бросал, было унаследовано от std::exception. Чтобы любой Петя смог хотя бы показать пользователю e.what();

Впрочем, я что то не понимаю.

Вот Вы пользователю что сообщаете, как разработчик функции some_class::some_function? Про ошибки.

Мой пользователь - это разработчик (т.к. я ему даю библиотеку).

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

А как было бы проще, если бы все просто бросали исключения, а строчка для показа - внутри, да?

Обработка ошибки - это же не только показ сообщения об этой ошибке пользователю.

Надо
- аккуратно все позакрывать-поосвобождать (включая всякие промежуточные буферы не на стеке)
- завершить threads (если были)
- ну и вообще, прибрать за собой рабочее место.
То есть в любом случае - нужно на промежуточном уровне (ниже пользователя библиотеки) это все делать.
Ну и сочетание с OpenMP (и прочими lightweight-тредами, вроде TBB) - неиллюзорно доставляет.

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

Вот Вы спрашивали про правила хорошего тона в посте? Так вот, ВСЕ перечисленное Вами категорически запрещается делать в обработчиках ошибок. Все это должно выполняться в деструкторах. Ссылки на объекты просто так не держать; все делать через смартпойнтеры.

Исключения в потоках - это отстой, да. Видел только одно решение хорошее - в .NET Framework 4.0.

Почему в деструкторах?

Вот у меня объект LibRaw, он может обработать много файлов. Надо ли ему ломаться и (self-)-деструктиться, если ему подсунули битый файл или такой путь в файловой системе, который не читается?

То есть о деструкторе речь не идет.

Алексей, если будет у Вас время (и желание), почитайте книгу "Стандарты программирования на C++. 101 правило и рекомендация", Герб Саттер, Андрей Александреску. Очень хорошо прочищает голову, лично мне сильно помогла эта книга.

С учетом того, что меня учили программированию году в 85-м и на фортране, мне вряд-ли уже что-то такое поможет.

Но посмотрю.

Да, я понимаю, мне тоже было в лом начинать ее читать :) Но там отдельный небольшие главы, удобно.

Я долистал до 39-го указания и понял, что меня тошнит.

Попробую английский вариант, на русском вот этот вот "Виртуальные функции следует делать неоткрытими, а открытые - невиртуальными" ввело меня в ступор (с третьего раза понятно, что открытые - public, но я в гробу видел).

Ну и вообще, первые 82 страницы - не понравились. Потому что "жизнь - богаче".

Тогда простите за вредный совет. Лично мою жизнь эта книга сделала существенно проще.

Ну, для начала, я вообще не люблю этот язык и считаю его избыточно сложным (и потому - уверенно знаю только некоторый subset, которым и пользуюсь). Это вообще.

А в частности, возможно виноват перевод, но мне кажется, что автор намеренно пытается сделать простые вещи - сложными. Может быть вследствие сложности языка, не знаю.

Долистал до 62-го совета.

Гуру рекомендует исключения из библиотеки не выпускать.

Вот и я так думаю. С той поправкой, что чужие (неизвестные) - пропускать через себя.

Вы распространяете свою библиотеку для разных -никсов в едином бинарном виде? Потому что только в этом случае действует эта рекомендация. Те же stl и boost замечательно бросают исключения.

В бинарном - только под винды и мак.

А stl/boost - это "базовые типы", там проверять каждый чих действительно устанешь. Но LibRaw - это 3-4 вызова на файл, можно и проверить бы.

А пользователь some_class::some_function(...) ожидает, что из нее может вылететь исключение? Если не ожидает, то вариантов нет: надо ловить все и преобразовывать в код возврата. Но при этом информация об ошибке может частично пропасть.
Если ожидает, то ему навязывается двойная обработка ошибок, что для него довольно неприятно.

В первом комментарии есть ссылка на пост Макса, где ясно написано, что catch(...) вреден для здоровья.

Не, я ни разу не призываю ловить фатальные ошибки, после которых только и остается, что ползти на кладбище. ;-) Я только о том, что наличие одновременно двух способов обработки ошибок не есть гут. Если исключению позволяется вылететь наружу, то не надо ему мешать это делать erroCode-ами. Ну, например:
catch( std:exception ) {
аккуратно_склеить_ласты();
throw;
}
Если надо отдельно обрабатывать my_own_exception_type, то его надо отдельно и словить. Но это странный подход, именно по причине двойной обработки ошибок _внутри_ вашего кода.

Ну, вообще я был уверен, что наружу - только errorcode (тем более, что есть C-интерфейс, какие уж там исключения).
Но несчастный юзер налетел на exception от потрохов (std::filebuf), я не понимаю что должно случиться, чтобы этот filebuf не смог прочитать первые два байта из файла (они там есть!), ну наверное там так ошибка ОС обрабатывается, киданием исключения. Отлично, std::exception от своей имплементации - я поймаю, нивапрос.

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

Такое поведение - нормальное? Засунул что-то нестандартное - лови сам!

Мнэээ, Полуэкт... Что-то начинает прояснятся, я видимо сначала не понял. ;-) Но все равно не хватает подробностей, приходится фантазировать. У вас C++-интерфейс, из которого ничего не должно вылетать. Но внутри вашего кода исключения есть, и их надо наверху преобразовывать в errorCodes. Так? Видимо std::filebuf - это такой callback, который приходит снаружи как параметр, так? Тогда глотать его исключения (заменять на errorCode) - плохо, потеря информации, пользователь обидится. Я бы оборачивал вызовы callback-а в try-catch с последующим rethrow, а преобразование в errorCode делал только со своими исключениями. Если это возможно. Ну или пробовал бы strict-подход, как рекомендуют выше: вся чистка в деструкторах, наружу сложные объекты с такими деструкторами не торчат, и т.д.
Рассчитывать на то, что пользовательский код будет кидать исключениями определенного типа также не стоит.

конкретно std::filebuf у меня внутри, но по сути ты прав.

Есть некоторый (базовый) класс, реализующий абстракцию I/O. По семантике это FILE* (fseek, fread, fscanf) с небольшими нашлепками, которые для сути дела неважны.
У меня в библиотеке реализованы три имплементации этого класса - для маленького файла (полная буферизация в памяти), для большого файла, для буфера в памяти.

Но пользователь может реализовать свое, скажем для сетевого сокета (fseek придется делать чтением до нужного места, но меня это не парит).
И пользовательская реализация может кидать свои исключения.

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

Функция-парсер, по семантике, возвращает код ошибки. По всей видимости, все исключения, о которых она может догадываться (свои собственнные и от "фирменных" реализаций I/O) она должна перехватывать.

Но что делать с неизвестными исключениями? Особенно с учетом того, что они могут просто летать от мультитредности (говорят вот pthreads в винде сделаны частично на исключениях)....

Ну, мысли такие: по идее контракт с пользователем по исключениям ничем логически не отличается от контракта по интерфейсу, и должен оговариваться. Проще всего договорится, что исключения не летят. Т.е. например fread сообщает об ошибке возвращаемым значением или еще чем-то. И пусть пользователь сам в своем объекте это обеспечивает.
Про неизвестные исключения - я не понимаю где и откуда они летят, поэтому сказать ничего не могу. Но если надо все-таки как-то защититься от неразумных пользователей, которые таки кидаются исключениями, то я видел два подхода. Один такой: catch(...), который транслирует неизвестные исключения в какой-нибудь фатальный error code, после которого разрешается только помереть. Вот например SAX-парсер от MSXML именно так и делает - при любых исключениях в callback-ах возвращает какой-то fatal. Для пользователя не очень удобно.
Второй подход: catch(...) -> чистка -> rethrow. Пользователь снаружи LibRaw сможет словить собственные исключения. Но я не знаю как тут с бинарной совместимостью по исключениям, если LibRaw собран одним компилятором, а пользовательский код - другим.
В любом случае, мне кажется, фантазировать "а какой бы мне еще тип исключения словить" не стоит, это контракт.

Про компилятор я вот не вполне понимаю такую вот мелкую деталь: у меня в LibRaw::open_datastream() передается объект производный от class LibRaw_abstract_datastream; у которого потом дергаются разные виртуальные методы.
Боюсь я, что разные компиляторы породят несовместимую таблицу виртуальных функций в любом разе, до исключений дело не дойдет.

А контракт такого типа
"ваш класс ввода-вывода может кидать исключения LibRaw_exceptions и std::exception, тогда они будут перехвачены и обработаны" - нормальная формулировка?

Ой, вот про разные компиляторы я вообще не копенгаген. Попробую спросить у кого-нибудь.

Насчет контракта, немного смущает вот что: пользователю навязывается, что для информирования об ошибке он должен кинуть исключение, а вот в ответ ему потом придет errorCode. Если это какие-то сетевые ошибки, то как он получит о них информацию? Хотя бы в виде строчки. Я поэтому и предлагал запретить исключения, раз уж все равно сведения теряются.

Тем более, если стремно полагаться на бинарную совместимость. Вот если layout объекта фиксирован, как в .NET, то ради бога. А вот например в COM фиксирован только layout интерфейса, поэтому исключения наружу не летают, ошибки передаются через глобальный объект IErrorInfo (затычка, как errno).

А собственные исключения юзерского класса - пройдут через мой интерфейс насквозь.

В-общем, наверное так и надо сделать (и так уже и сделано, собственно, осталось описать)

Не говоря о том, что тот экземпляр IO-класса, который мне передали для IO - он остался у пользователя и можно его просто опросить "что случилос" (да, он должен состояние для этого хранить).

Да, про деструкторы.

Откуда берется идея, что они вообще есть?
В приложении может быть один глобальный объект LibRaw raw_files_parser;
Вызов raw_files_parser.recycle() приводит его в исходное состояние, деструктор не нужен.