Is `catch (…) {кинути; } `погана практика?


74

Хоча я погоджуюся, що ловити ... без повторного скидання дійсно неправильно, проте я вважаю, що використовуючи такі конструкції:

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

Є чи прийнятним в тих випадках , коли RAII не застосовують . (Будь ласка, не питайте ... не всі в моїй компанії люблять об’єктно-орієнтоване програмування, і RAII часто сприймається як "непотрібні шкільні речі" ...)

Мої колеги кажуть, що ви завжди повинні знати, які винятки потрібно кинути, і що ви завжди можете використовувати такі конструкції:

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

Чи є добре сприйнята добра практика щодо цих ситуацій?


3
@Pubby: Не впевнений, що це саме той самий питання. Пов’язане запитання стосується більше "Чи повинен я ловити ...", тоді як моє питання зосереджується на "Чи слід краще ловити ...або <specific exception>перед тим, як відкинути"
ereOn

53
Вибачте, що можу сказати, але C ++ без RAII не є C ++.
fredoverflow

46
Тож ваші корівники відкидають техніку, яку винайшли для вирішення певної проблеми, а потім посперечаються, яку з нижчих альтернатив слід використовувати? Вибачте, але це здається нерозумним , незалежно від того, на який спосіб я б це не дивився.
sbi

11
"ловити ... без повторного скидання справді неправильно" - ви помиляєтесь. В main, catch(...) { return EXIT_FAILURE; }може, правильно в коді, який не працює під налагоджувачем. Якщо ви не ловите, то стек може не розмотатися. Лише тоді, коли ваш налагоджувач виявить невловимі винятки, ви захочете їх залишити main.
Стів Джессоп

3
... так що навіть якщо це "помилка програмування", це не обов'язково випливає, що ви не хочете про це знати. У будь-якому разі, ваші колеги не є хорошими професіоналами програмного забезпечення, тому, як sbi каже, дуже важко говорити про те, як найкраще впоратися з ситуацією, яка починається з хронічної слабкості.
Стів Джессоп

Відповіді:


196

Мої колеги кажуть, що ви завжди повинні знати, які винятки потрібно кинути [...]

Ваш колега, я б ненавиджу це сказати, явно ніколи не працював над бібліотеками загального призначення.

Як у світі такий клас, як std::vectorнавіть може зробити вигляд, що знає, що кинуть конструктори копій, при цьому все одно гарантуючи безпеку винятків?

Якщо ви завжди знали, що буде робити калле в час компіляції, то поліморфізм був би марним! Іноді вся мета полягає в тому, щоб відмовитись від того, що відбувається на більш низькому рівні, тому ви конкретно не хочете знати, що відбувається!


32
Власне, навіть якби вони знали, що викиди потрібно кидати. Яка мета цього дублювання коду? Якщо обробка не відрізняється, я не бачу сенсу перераховувати винятки, щоб показати свої знання.
Майкл Крелін - хакер

3
@ MichaelKrelin-хакер: Це теж. Крім того, додайте до нього той факт, що вони застаріли в специфікаціях винятків, оскільки перелік усіх можливих винятків у коді, як правило, викликав помилки згодом ... це найгірша ідея коли-небудь.
Мехрдад

4
І мене хвилює те, що може бути джерелом такого ставлення, коли поєднується із баченням корисної та зручної техніки як "марної шкільної речі". Але добре ...
Майкл Крелін - хакер

1
+1, перерахування всіх можливих варіантів - чудовий рецепт майбутньої невдачі, чому б на землі хтось обрав би це зробити ...?
littleadv

2
Гарна відповідь. Можливо, було б корисно згадати, що якщо компілятор, який потрібно підтримувати, має помилку в області X, то використовувати функціональність з області X не розумно, принаймні не використовувати його безпосередньо. Наприклад, враховуючи інформацію про компанію, я не був би здивований, якби вони використовували Visual C ++ 6.0, у якому в цій області було кілька дурних помилок (як, наприклад, деструктори винятків, які викликаються двічі) - деякі менші нащадки цих ранніх помилок збереглися до цього дня, але вимагають ретельної домовленості, щоб проявитись.
Альф П. Штейнбах

44

Те, що вам здається, потрапило в певне пекло того, хто намагається випити їх торт і з'їсти його теж.

RAII та винятки призначені для того, щоб йти рука об руку. RAII - це засіб, за допомогою якого вам не потрібно писати багато catch(...)заяв, щоб зробити очищення. Це відбудеться автоматично, як зрозуміло. А винятки - єдиний спосіб роботи з об’єктами RAII, оскільки конструктори можуть лише домогтися успіху або кинути (або поставити об'єкт у стан помилки, але хто цього хоче?).

catchЗаява може зробити один з двох речей: обробляти помилки або виняткові обставини, або робити роботу по очищенню. Іноді це робить і те, і інше, але кожне catchтвердження існує, щоб зробити принаймні одне з них.

catch(...)не може зробити належну обробку виключень. Ви не знаєте, що таке виняток; ви не можете отримати інформацію про виняток. У вас немає абсолютно ніякої інформації, крім того, що виняток було кинуто чимось у певному кодовому блоці. Єдине законне, що ви можете зробити в такому блоці - це зробити очищення. А це означає перекинути виняток наприкінці очищення.

Що дає RAII щодо обробки винятків, це безкоштовне очищення. Якщо все RAII інкапсульовано належним чином, то все буде належним чином очищено. Вам більше не потрібно, щоб catchзаяви чистили. У такому випадку писати catch(...)заяву немає підстав .

Тож я погодився б, що catch(...)це злість ... тимчасово .

Це положення є належним використанням RAII. Тому що без цього потрібно вміти робити певну очистку. Не обійти його; ви повинні вміти робити прибирання. Потрібно мати можливість переконатися, що кидання винятку залишить код у розумному стані. І catch(...)це життєво важливий інструмент для цього.

Не можна мати одне без іншого. Ви не можете сказати, що і RAII, і catch(...) погані. Вам потрібно хоча б одне із них; інакше ти не безпечний виняток.

Звичайно, є одне дійсне, хоча і рідкісне використання, catch(...)яке навіть RAII не може прогнати: отримання exception_ptrпереадресації комусь іншому. Зазвичай через promise/futureчи подібний інтерфейс.

Мої колеги кажуть, що ви завжди повинні знати, які винятки потрібно кинути, і що ви завжди можете використовувати такі конструкції:

Ваш колега - ідіот (або просто жахливо необізнаний). Це повинно бути відразу очевидним через те, скільки копіювати та вставляти код він пропонує вам написати. Очищення для кожної з цих заяв на вилов буде точно однаковою . Це кошмар технічного обслуговування, не кажучи вже про читанність.

Коротше кажучи: це проблема, яку RAII створили для вирішення (не те, що вона не вирішує інших проблем).

Що мене бентежить у цьому понятті, це те, що воно, як правило, відстало від того, як більшість людей стверджує, що RAII поганий. Як правило, аргумент "RAII поганий, тому що ви повинні використовувати винятки для відмови конструктора сигналів. Але винятки не можете кидати, тому що це не безпечно, і вам доведеться мати багато catchтверджень, щоб очистити все". Що є неполадним аргументом, тому що RAII вирішує проблему, яку створює брак RAII.

Більш ніж ймовірно, він проти RAII, оскільки він приховує деталі. Виклики деструктора не видно відразу в автоматичних змінних. Таким чином, ви отримуєте код, який викликається неявно. Деякі програмісти дуже ненавиджу це. Судячи з усього, catchкраща ідея, якщо вони думають, що мають 3 твердження, всі вони роблять те ж саме з кодом копіювання та вставки.


2
Здається, ви не пишете код, який забезпечує чітку гарантію безпеки винятків. RAII допомагає забезпечити базову гарантію. Але для забезпечення надійної гарантії вам потрібно скасувати деякі дії щодо повернення системи до стану, до якого вона мала функцію до виклику функції. Основна гарантія - це очищення , гарантія - відкат . Відкат залежить від функції. Таким чином, ви не можете помістити його в "RAII". І це тоді, коли блок -лов все стає в нагоді. Якщо ви пишете код з високою гарантією, ви багато використовуєте catch .
anton_rh

@anton_rh: Можливо, але навіть у цих випадках усі висловлювання є інструментом останнього засобу . Кращим інструментом є зробити все, що кидається, перш ніж змінити будь-який стан, який вам доведеться повернути за винятком. Очевидно, що ви не можете реалізувати все таким чином у всіх випадках, але це ідеальний спосіб отримати гарантію виключення.
Нікол Болас

14

Два коментарі, дійсно. Перший полягає в тому, що, перебуваючи в ідеальному світі, ви завжди повинні знати, які винятки можуть бути викинуті, на практиці, якщо ви маєте справу з сторонніми бібліотеками або збираєте компілятор Microsoft, ви цього не робите. Однак, до речі, більше; навіть якщо ви точно знаєте всі можливі винятки, це актуально тут? catch (...)висловлює наміри набагато краще, ніж catch ( std::exception const& )навіть припускаючи, що всі можливі винятки походять із цього питання std::exception(що було б в ідеальному світі). Що стосується використання декількох блоків вилову, якщо для всіх винятків немає спільної бази: це прямо затуманення і кошмар підтримки. Як ви визнаєте, що всі форми поведінки однакові? І це був намір? А що станеться, якщо вам доведеться змінити поведінку (наприклад, виправлення помилок)? Це занадто просто пропустити.


3
Насправді мій колега розробив власний клас виключень, який не випливає з std::exceptionта щоденно намагається застосувати його використання серед нашої кодової бази. Я здогадуюсь, що він намагається покарати мене за використання коду та зовнішніх бібліотек, які він сам не написав.
ereOn

17
@ereOn Мені здається, що ваш колега гостро потребує навчання. У будь-якому випадку я, мабуть, уникав би використовувати бібліотеки, написані ним.

2
Шаблони і знаючи, які винятки будуть кинуті, збираються разом, як арахісове масло та мертві гекони. Щось таке просте, що std::vector<>може кинути будь-який виняток з майже будь-якої причини.
Девід Торнлі

3
Скажіть, будь ласка, як саме ви знаєте, який виняток буде кинутий завтрашній виправлення помилки далі вниз по дереву дзвінків?
mattnz

11

Я думаю, що ваш колега змішав кілька корисних порад - вам слід керувати відомими винятками в catchблоці лише тоді, коли ви їх не кидаєте заново.

Це означає:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

Це погано, тому що воно мовчки приховає будь-яку помилку.

Однак:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

Це добре - ми знаємо, з чим маємо справу, і не потрібно піддавати це коду виклику.

Аналогічно:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

Добре, навіть найкраща практика, код для вирішення загальних помилок повинен бути з кодом, який їх викликає. Краще, ніж покладатися на абонента, щоб знати, що транзакція потребує відхилення або будь-якого іншого.


9

Будь-яка відповідь " так" чи " ні" повинна супроводжуватися обґрунтуванням того, чому це так.

Сказати, що це неправильно просто тому, що мене так вчили, - це просто сліпий фанатизм.

Писати те саме //Some cleanup; throwкілька разів, як у вашому прикладі неправильно, оскільки це дублювання коду, і це тягар для обслуговування. Написати це лише один раз краще.

Писати, catch(...)щоб замовчувати всі винятки, неправильно, тому що ви маєте поводитися лише з винятками, з якими ви знаєте, як керувати, і за допомогою цього підключення ви можете подати більше, ніж ви очікуєте, і це може заглушити важливі помилки.

Але якщо ви повторно скидаєтесь після a catch(...), то останнє обгрунтування більше не застосовується, оскільки ви насправді не обробляєте виняток, тому немає ніяких причин, чому це не варто перешкоджати.

Насправді я це робив для входу в чутливі функції без жодної проблеми:

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}

2
Будемо сподіватися, що Log(...)не може кинути.
Дедуплікатор

2

Я загалом погоджуюся з настроєм публікацій тут, мені дуже не подобається схема вибору конкретних винятків - я думаю, що синтаксис цього ще знаходиться в зародковому стані і ще не в змозі впоратися із зайвим кодом.

Але оскільки всі говорять про це, я погоджуся з тим, що, хоча я їх використовую економно, я часто переглядав одну зі своїх заяв про "вилов (виняток е)" і сказав: "Чорт, я хотів би зателефонувати з винятків конкретних винятків того часу ", тому що, коли ви приїдете пізніше, часто приємно знати, який був намір і що клієнт, швидше за все, кине з першого погляду.

Я не виправдовую ставлення до "Завжди використовуй х", просто кажу, що іноді приємно бачити їх у списку, і я впевнений, що тому деякі люди думають, що це "правильний" шлях.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.