Почему спецификации исключений плохие?

50

В школе более 10 лет назад они учили вас использовать спецификаторы исключений. Так как мой фон, как один из них Torvaldish Программисты, которые упорно избегает C ++, если не принуждал, я только в конечном итоге в C ++ спорадически, и когда я делаю, я до сих пор используют исключения спецификаторов, поскольку это то, что меня учили.

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

  1. Спецификаторы исключений используют систему типов, которая несовместима с остальной частью языка («система теневых типов»).
  2. Если ваша функция со спецификатором исключения выбрасывает что-то еще, кроме того, что вы указали, программа будет завершена неудачным, неожиданным образом.
  3. Спецификаторы исключений будут удалены в следующем стандарте C ++.

Я что-то здесь упускаю или это все причины?

Мои собственные мнения:

По поводу 1): ну и что. C ++, вероятно, самый противоречивый язык программирования, когда-либо созданный, с точки зрения синтаксиса. У нас есть макросы, переходы / метки, полчища (неопределенность?) Поведения неопределенного / неопределенного / определяемого реализацией, плохо определенные целочисленные типы, все неявные правила продвижения типов, ключевые слова специального случая, такие как friend, auto , зарегистрируйтесь, явный ... И так далее. Кто-то, возможно, мог бы написать несколько толстых книг всех странностей в C / C ++. Так почему же люди реагируют на это конкретное несоответствие, которое является незначительным недостатком по сравнению со многими другими, гораздо более опасными особенностями языка?

Относительно 2): Разве это не моя ответственность? Есть так много других способов, которыми я могу написать фатальную ошибку в C ++, почему этот конкретный случай хуже? Вместо того, чтобы писать, throw(int)а затем выдавать Crash_t, я могу также утверждать, что моя функция возвращает указатель на int, затем создать дикий, явный тип-преобразование и вернуть указатель на Crash_t. Дух C / C ++ всегда заключался в том, чтобы оставить большую часть ответственности программисту.

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

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

void func() throw(Egg_t);

а также

void func(); // This function throws an Egg_t

Я думаю, что есть большая вероятность, что вызывающая сторона игнорирует / забывает реализовать try-catch во втором случае, а в первом - меньше.

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

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

И с точки зрения пользователя им было бы наплевать, если они получат окно с злым сообщением из ОС, говорящее «Программа остановлена. Blablabla по адресу 0x12345», или окно с злым сообщением из вашей программы, говорящее «Необработанное исключение: myclass. func.something». Ошибка все еще там.


С будущим стандартом C ++ у меня не будет другого выбора, кроме как отказаться от спецификаторов исключений. Но я бы скорее услышал какой-то веский аргумент, почему они плохие, чем «Его Святейшество заявил об этом, и поэтому это так». Возможно, против них больше аргументов, чем перечисленных мною, или, может быть, их больше, чем я понимаю?


источник
30
Я испытываю соблазн понизить это как "напыщенная речь, замаскированная под вопрос". Вы спрашиваете три верных момента о E.specs, но почему вы беспокоите нас своими болтовнями C ++ - это так раздражает и мне нравится - C-лучше?
Мартин Ба,
10
@Martin: Потому что я хочу отметить, что я и предвзят, и не в курсе всех деталей, но и что я отношусь к языку наивными и / или еще не испорченными глазами. Но также то, что C и C ++ уже невероятно испорченные языки, поэтому один недостаток более или менее не имеет значения. Пост был на самом деле намного хуже, прежде чем я его отредактировал :) Аргументы против спецификаторов исключений также весьма субъективны, и поэтому их трудно обсуждать, не получая субъективных результатов самостоятельно.
SpanishInquisition_t! Веселое! Лично я был впечатлен использованием указателя стекового фрейма для создания исключений, и кажется, что он может сделать код намного чище. Тем не менее, я никогда не писал код с исключениями. Назовите меня старомодным, но возвращаемые значения прекрасно работают для меня.
Шахбаз
@ Shahbaz Как можно читать между строк, я тоже довольно старомоден, но все же я никогда не задавался вопросом, являются ли исключения сами по себе хорошими или плохими.
2
Прошедшее время броска является метал , не бросило . Это сильный глагол.
TRiG

Ответы:

48

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

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

DeadMG
источник
2
Но проверка во время выполнения не является ошибкой спецификации исключения как таковой, а скорее связана с какой-либо частью стандарта, которая гласит, что должно быть прекращение, если выдается неправильный тип. Разве не было бы разумнее просто удалить требование, приводящее к этой динамической проверке, и позволить всему статически оценивать компилятор? Похоже, что RTTI - настоящий виновник здесь, или ...?
7
@Lundin: невозможно статически определить все исключения, которые могут быть выброшены из функции в C ++, потому что язык допускает произвольные и недетерминированные изменения в потоке управления во время выполнения (например, через указатели функций, виртуальные методы и как). Реализация статической проверки исключений во время компиляции, как в Java, потребует фундаментальных изменений в языке и отключения C-совместимости.
3
@OrbWeaver: Я думаю, что статические проверки вполне возможны - спецификация исключения будет частью спецификации функции (например, возвращаемого значения), а указатели функций, виртуальные переопределения и т. Д. Должны иметь совместимые спецификации. Основное возражение заключается в том, что это функция «все или ничего» - функции со статическими спецификациями исключений не могут вызывать устаревшие функции. (Вызов функций C был бы нормальным, так как они не могут ничего генерировать).
Майк Сеймур
2
@Lundin: Спецификации исключений не были удалены, просто устарели. Устаревший код, который их использует, все равно будет действительным.
Майк Сеймур
1
@Lundin: старые версии C ++ со спецификациями исключений полностью совместимы. Они не могут ни скомпилироваться, ни выполнить неожиданно, если код был действителен раньше.
DeadMG
18

Одна из причин, по которой никто не использует их, заключается в том, что ваше основное предположение неверно:

«Наиболее очевидное [преимущество] заключается в том, что если ваша функция пытается явно выдать любой тип, отличный от указанного вами, компилятор выдаст вам ошибку».

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Эта программа компилируется без ошибок или предупреждений .

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


Изменить: чтобы ответить на вопрос в вашем вопросе, ловить (...) (или лучше, ловить (std :: exeption &) или другой базовый класс, а затем ловить (...)) по-прежнему полезно, даже если вы этого не сделаете точно знать, что пошло не так.

Учтите, что ваш пользователь нажал кнопку меню для «сохранения». Обработчик для кнопки меню предлагает приложению сохранить. Это может произойти сбой по множеству причин: файл находился на сетевом ресурсе, который исчез, был файл только для чтения, и его нельзя было сохранить и т. Д. Но обработчику на самом деле все равно, почему что-то не удалось; его волнует только то, что он либо преуспел, либо провалился. Если это удалось, все хорошо. Если это не удалось, он может сказать пользователю. Точная природа исключения не имеет значения. Кроме того, если вы пишете правильный, безопасный для исключений код, это означает, что любая такая ошибка может распространяться через вашу систему, не останавливая ее - даже для кода, который не заботится об ошибке. Это облегчает расширение системы. Например, теперь вы сохраняете через соединение с базой данных.

Каз Дракон
источник
4
@Lundin скомпилирован без предупреждений. И нет, ты не можешь. Ненадежно. Вы можете вызывать с помощью указателей на функции, или это может быть виртуальная функция-член, и производный класс генерирует output_exception (о котором базовый класс не может знать).
Каз Драгон
Везет, как утопленнику. Embarcadero / Borland выдает предупреждение за это: «исключение выброса нарушает спецификатор исключения». Видимо GCC нет. Нет никаких причин, по которым они не могли бы реализовать предупреждение. И аналогично, инструмент статического анализа может легко найти эту ошибку.
14
@Lundin: Пожалуйста, не начинайте спорить и повторять ложную информацию. Ваш вопрос уже был чем-то напыщенным, и утверждение о ложных вещах не помогает. Инструмент статического анализа НЕ МОЖЕТ найти, если это произойдет; Для этого потребуется решить проблему остановки. Более того, необходимо проанализировать всю программу, и очень часто весь источник недоступен.
Дэвид Торнли
1
@Lundin тот же инструмент статического анализа может сказать вам, какие сгенерированные исключения покидают ваш стек, поэтому в этом случае использование спецификаций исключений все равно ничего не даст вам, кроме потенциального ложноотрицательного результата, который приведет к падению вашей программы, где вы могли бы обработать случай ошибки в гораздо более приятный способ (например, объявление о сбое и продолжение работы: см. вторую половину моего ответа для примера).
Каз Драгон
1
@Lundin Хороший вопрос. Ответ в том, что, в общем, вы не знаете, будут ли вызываемые вами функции генерировать исключения, поэтому можно предположить, что все они делают. Где поймать это вопрос, на который я ответил в stackoverflow.com/questions/4025667/… . В частности, обработчики событий формируют естественные границы модулей. Разрешение исключениям попадать в общее окно / систему событий (например), будет наказано. Обработчик должен поймать их всех, если программа хочет там выжить.
Каз Драгон
17

Это интервью с Андерсом Хейлсбергом довольно известно. В нем он объясняет, почему команда разработчиков C # вообще отказалась от проверенных исключений. В двух словах, есть две основные причины: возможность обновления и масштабируемость.

Я знаю, что OP фокусируется на C ++, тогда как Хейлсберг обсуждает C #, но замечания Хейлсберга прекрасно применимы и к C ++.

CesarGon
источник
5
«Точки, которые делает Хейлсберг, вполне применимы и к C ++» .. Неверно! Проверенные исключения, реализованные в Java, заставляют вызывающего обработать их (и / или вызывающего вызывающего и т. Д.). Спецификации исключений в C ++ (хотя они довольно слабые и бесполезные) применяются только к одной «границе вызова функции» и не «распространяются вверх по стеку вызовов», как это делают проверенные исключения.
Мартин Ба
3
@Martin: я согласен с вами относительно поведения спецификаций исключений в C ++. Но моя фраза, которую вы цитируете «пункты, которые делает Хейлсберг, вполне применимы и к C ++», относится, скорее, к пунктам, которые делает Хейлсберг, а не к спецификациям C ++. Другими словами, я говорю здесь о версии и масштабируемости программы, а не о распространении исключений.
CesarGon
3
Я довольно рано не согласился с Хейлсбергом: «Меня беспокоит проверяемые исключения - это наручники, которые они надевают на программистов. Вы видите, как программисты подбирают новые API-интерфейсы со всеми этими предложениями throws, а затем вы видите, насколько запутанным становится их код, и понимаете, проверенные исключения не помогают им. Это своего рода диктаторские разработчики API, говорящие вам, как обрабатывать исключения ». --- Проверяются исключения или нет, вам все равно придется обрабатывать исключения, создаваемые вызываемым вами API. Проверенные исключения делают просто явным.
Quant_Dev
5
@quant_dev: Нет, вам не нужно обрабатывать исключения, создаваемые API, который вы вызываете. Совершенно допустимый вариант - не обрабатывать исключения и позволять им пузыриться в стеке вызовов для обработки вашим вызывающим абонентом.
CesarGon
4
@CesarGon Не обработка исключений также делает выбор, как их обрабатывать.
Quant_dev