Як я можу створювати та застосовувати контракти за винятками?


33

Я намагаюся переконати свою команду в тому, щоб дозволити використовувати винятки в C ++, а не повертати bool isSuccessfulабо enum з кодом помилки. Однак я не можу протистояти цій критиці його.

Розглянемо цю бібліотеку:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. Розглянемо розробник, який викликає B()функцію. Він перевіряє його документацію і бачить, що вона не повертає винятків, тому нічого не намагається зловити. Цей код може зірвати програму у виробництві.

  2. Розглянемо розробник, який викликає C()функцію. Він не перевіряє документацію, тому не виключає жодних винятків. Виклик небезпечний і може зірвати програму у виробництві.

Але якщо ми перевіримо помилки таким чином:

void old_C(myenum &return_code);

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

Як я можу безпечно використовувати винятки, щоб був якийсь контракт?


4
@Sjoerd Основна перевага полягає в тому, що розробник змушений компілятором надати змінну функції, щоб зберігати код повернення; він усвідомлює, що може статися помилка, і він повинен її виправити. Це нескінченно краще, ніж винятки, у яких взагалі немає перевірки часу компіляції.
DBedrenko

4
"він повинен з цим впоратися" - перевірки, чи це трапляється, немає.
Sjoerd

4
@Sjoerd Це не обов'язково. Це досить добре, щоб повідомити розробника про те, що може статися помилка, і що вони повинні її перевірити. Зрозуміло, що рецензенти теж бачать, перевіряли вони на помилку чи ні.
DBedrenko

5
У вас може бути більший шанс за допомогою Either/ Resultmonad повернути помилку зручним для друку способом
Daenyth

5
@gardenhead Оскільки занадто багато розробників не використовують його належним чином, в кінцевому підсумку з некрасивим кодом і приступають до звинувачення цієї функції замість себе. Функція перевірених винятків на Java - це прекрасна річ, але вона отримує поганий реп, оскільки деякі люди просто не розуміють цього.
AxiomaticNexus

Відповіді:


47

Це правомірна критика винятків. Вони часто менш помітні, ніж прості поводження з помилками, такі як повернення коду. І немає простого способу примусового виконання «контракту». Частина полягає в тому, щоб ви могли перешкоджати виняткам на більш високому рівні (якщо вам доведеться виловлювати всі винятки на кожному рівні, чим він відрізняється від повернення коду помилки?). А це означає, що ваш код може викликати якийсь інший код, який не обробляє його належним чином.

Винятки мають і мінуси; ви повинні скласти випадок, виходячи із вартості та вигоди.
Ці дві статті я вважаю корисними: Необхідність винятків та Все, що не так, з винятками. Крім того, у цій публікації в блозі пропонується думка багатьох експертів щодо винятків з акцентом на C ++ . Хоча думка експертів, здається, схиляється на користь винятків, це далеко не явний консенсус.

Що стосується переконання лідерства вашої команди, то це може бути не правильним вибором. Особливо не зі спадковим кодом. Як зазначено у другому посиланні вище:

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

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

Це, мабуть, не аргумент, на який я б витратив зусилля - принаймні, не до того, як буде розпочато новий проект. І навіть якщо у вас є новий проект, чи буде він використаний чи використаний будь-яким спадковим кодом?


1
Дякую за пораду та посилання. Це новий проект, а не спадщина. Мені цікаво, чому винятки настільки широко поширені, коли вони мають такий великий недолік, що робить їх використання небезпечним. Якщо ви виберете виняток n-рівнів вище, то пізніше ви модифікуєте код і переміщуєте його, не проводиться статична перевірка, щоб гарантувати, що виняток все-таки потрапляє та обробляється (і надто багато просити рецензентів перевірити всі функції n-рівнів глибокі ).
DBedrenko

3
@Dee див. Посилання вище для деяких аргументів на користь винятків (я додав додаткове посилання з більшою думкою експерта).

6
Ця відповідь на місці!
Doc Brown

1
Я можу сказати з досвіду, що намагання додати винятки до вже існуючої бази коду - СУПАДЬ погана ідея. Я працював з такою кодовою базою, і це було ДІЙСНО, ДУЖЕ важко працювати, тому що ти ніколи не знав, що може підірватися тобі в обличчя. Винятки слід використовувати ТІЛЬКИ у проектах, де ви плануєте їх наперед.
Майкл Коне

26

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


Винятки призначені НЕ, щоб їх ловили всюди.

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

У моєму коді є лише декілька висловлювань про вилов - за межами основного циклу. І це, мабуть, є загальним підходом у сучасному C ++.


Винятки та коди повернення не є взаємовиключними.

Ви не повинні робити це підходом все-таки або нічого. Винятки слід використовувати у виняткових ситуаціях. Такі речі, як "Налаштування файлу не знайдено", "Диск повний" або що-небудь інше, що не може бути оброблено локально.

Поширені збої, як-от перевірка того, чи є ім'я файлу, надане користувачем, дійсним, не є випадком використання для виключень; Використовуйте натомість значення повернення.

Як ви бачите з наведених вище прикладів, "файл не знайдено" може бути винятком або кодом повернення, залежно від випадку використання: "є частиною установки" проти "користувач може зробити друк".

Тож немає абсолютного правила. Приблизним орієнтиром є: якщо з ним можна поводитися локально, зробіть це зворотним значенням; якщо ви не можете впоратися з цим локально, киньте виняток.


Статична перевірка винятків не корисна.

Оскільки винятки не обробляються локально, зазвичай не важливо, які винятки можна кинути. Єдина корисна інформація - чи можна викинути якийсь виняток.

У Java є статична перевірка, але зазвичай це вважається невдалим експериментом, і більшість мов, оскільки - особливо C # - не мають такого типу статичної перевірки. Це добре прочитане про причини, чому C # не має цього.

З цих причин C ++ застаріло throw(exceptionA, exceptionB)на користь noexcept(true). За замовчуванням функція може кидати, тому програмісти повинні очікувати, що якщо документація прямо не передбачає іншого.


Написання безпечного коду виключення не має нічого спільного з написанням обробників виключень.

Я хотів би сказати, що написання безпечного коду виключення - це все, як уникнути написання обробників виключень!

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


3
" Поки існує загальний обробник винятків ... з вами зазвичай вже нормально ". Це суперечить більшості джерел, які підкреслюють, що писати безпечний код для винятків дуже складно.

12
@ dan1111 "Писання безпечного коду виключення" мало стосується написання обробників виключень. Написання безпечного коду для винятків - це використання RAII для інкапсуляції ресурсів, використання "спочатку всі роботи локально, тоді використовуйте swap / move" та інші найкращі практики. Це складно, коли ти до них не звик. Але "написання обробників виключень" не є частиною цих найкращих практик.
Sjoerd

4
@AxiomaticNexus На якомусь рівні ви можете пояснити будь-яке невдале використання функції як "поганих розробників". Найбільші ідеї - це ті, що знижують погане використання, оскільки роблять правильну справу просто. Статично перевірені винятки не належать до цієї категорії. Вони створюють роботу, непропорційну їхній цінності, часто в місцях, де написаний код просто не потребує турботи про них. Користувачі спокушаються замовкнути їх неправильними способами, щоб змусити компілятор перестати їх турбувати. Навіть зроблені правильно, вони призводять до безладу.
jpmc26

4
@AxiomaticNexus Причина, яка непропорційна, полягає в тому, що це може легко поширюватися на майже кожну функцію у всій вашій кодовій базі . Коли всі функції половини моїх класів отримують доступ до бази даних, не кожній з них потрібно чітко заявити, що вона може кинути SQLException; також не кожен метод контролера, який їх викликає. Цей багато шуму насправді є негативним значенням, незалежно від того, наскільки невеликий обсяг роботи. Sjoerd вдарив цвяхом по голові: більшість вашого коду не повинні стосуватися винятків, тому що він нічого не може зробити з ними .
jpmc26

3
@AxiomaticNexus Так, тому що кращі розробники настільки досконалі, що їх код завжди буде справлятись із кожним можливим входом користувачів, і ви ніколи не побачите цих винятків у продажі. А якщо ви це зробите, це означає, що ви невдача в житті. Серйозно, не дай мені цього сміття. Ніхто не такий ідеальний, навіть не найкращий. І ваш додаток має поводитись добросовісно навіть перед тими помилками, навіть якщо "розумним" є "зупинити і повернути наполовину пристойну сторінку / повідомлення про помилку". І здогадайтесь що: це те саме, що ви робите і з 99% IOExceptions . Перевірений поділ довільний і марний.
jpmc26

8

Програмісти на C ++ не шукають специфікацій виключень. Вони шукають гарантії виключення.

Припустимо, фрагмент коду кинув виняток. Які припущення може зробити програміст, що все ще буде дійсним? Що з написаного коду, що гарантує код після виключення?

Або можливо, що певний фрагмент коду може гарантувати ніколи не кидати (тобто нічого, крім того, що процес ОС завершується)?

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

Різні методи програмування на C ++ пропонують гарантії виключення. RAII (управління ресурсами на основі сфери) забезпечує механізм виконання коду очищення та забезпечення вивільнення ресурсів як у звичайних, так і у виняткових випадках. Створення копії даних перед виконанням модифікацій на об'єктах дозволяють відновити стан цього об’єкта, якщо операція не завершиться. І так далі.

Відповіді на це питання StackOverflow дають уявлення про велику довжину програмістів C ++, щоб зрозуміти всі можливі режими відмов, які можуть трапитися з їх кодом, і намагаються захистити дійсність програмного стану, незважаючи на збої. Лінійний аналіз коду С ++ стає звичною.

Розробляючи C ++ (для виробничого використання), ви не можете дозволити собі заглядати деталі. Крім того, бінарний блоб (не з відкритим кодом) є основою програмістів на C ++. Якщо мені доведеться зателефонувати на якийсь двійковий блоб, а крап не вдасться, то зворотний інжиніринг - це те, що далі робитиме програміст C ++.

Довідка: http://en.cppreference.com/w/cpp/language/exceptions#Exception_safety - див. Розділ Безпека виключень.

C ++ зазнала невдалої спроби впровадження специфікацій виключень. Пізній аналіз іншими мовами говорить про те, що специфікації виключень просто не практичні.

Чому це невдала спроба: суворо застосовувати це, воно повинно бути частиною системи типу. Але це не так. Компілятор не перевіряє специфікацію виключень.

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

Довідка: http://www.gotw.ca/publications/mill22.htm

Починаючи з C ++ 17, [[nodiscard]]атрибут можна використовувати для повернення значень функції (див. Https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value -може бути ігнорованим ).

Отже, якщо я вніс зміну коду, і це вводить новий вид відмови (тобто новий тип винятку), чи це невід'ємна зміна? Чи повинно було змусити абонента оновити код або принаймні попередити про зміну?

Якщо ви приймаєте аргументи, що програмісти C ++ шукають гарантії виключення замість специфікацій винятків, то відповідь полягає в тому, що якщо новий вид відмови не порушує жоден виняток, гарантує код, який раніше обіцяв, це не є переломною зміною.


"Оскільки ви модифікуєте реалізацію функції (наприклад, їй потрібно здійснити виклик до іншої функції, яка може запускати новий вид винятку), суворе виконання специфікації виключення означає, що вам доведеться також оновити цю специфікацію" - Добре , як слід. Яка була б альтернатива? Ігнорувати нещодавно внесений виняток?
AxiomaticNexus

@AxiomaticNexus На це також відповідає гарантія винятків. Насправді я стверджую, що специфікація ніколи не може бути повною; гарантія - це те, що ми шукаємо. Часто ми порівнюємо тип винятку, намагаючись з’ясувати, яка гарантія була порушена - щоб здогадатися, яка гарантія все ще діяла. Специфікація винятків перераховує деякі типи, які потрібно перевірити, але пам’ятайте, що в будь-якій практичній системі список не може бути повним. Здебільшого він змушує людей скористатися ярликом "їсти" тип винятку, загорнувши його в інший виняток, що робить перевірку типу виключень важкою
rwong

@AxiomaticNexus Я не сперечаюся за або проти використання винятку. Типове використання виключень заохочує базовий клас виключень та деякі похідні класи виключень. Тим часом потрібно пам’ятати, що можливі й інші типи винятків, наприклад, викинуті з C ++ STL або інших бібліотек. Можна стверджувати, що специфікація винятків може привести до спрацьовування в цьому випадку: вкажіть, що стандартні винятки ( std::exception) та ваш власний базовий виняток будуть викинуті. Але якщо застосувати до цілого проекту, це просто означає, що кожна функція матиме таку саму специфікацію: шум.
rwong

Я зазначу, що якщо мова йде про винятки, C ++ та Java / C # - це різні мови. По-перше, C ++ не є безпечним для типу, в тому сенсі, що дозволяє довільне виконання коду або перезапис даних, по-друге, C ++ не друкує приємний слід стека. Ці відмінності змусили програмістів C ++ робити різний вибір з точки зору обробки помилок та винятків. Це також означає, що певні проекти можуть законно стверджувати, що C ++ не є для них підходящою мовою. (Наприклад, я особисто вважаю, що декодування зображень TIFF заборонено застосовувати в C або C ++.)
rwong

1
@rwong: Велика проблема з обробкою винятків на мовах, які слідують моделі C ++, полягає в тому, що catchґрунтується на типі об'єкта винятку, коли в багатьох випадках важлива не стільки пряма причина винятку, скільки швидше те, що воно має на увазі стан системи. На жаль, тип винятку взагалі нічого не говорить про те, чи спричинив код зривний вихід фрагмента коду таким чином, що він залишив об'єкт у частково оновленому (і, таким чином, недійсному) стані.
supercat

4

Розглянемо розробника, який викликає функцію C (). Він не перевіряє документацію, тому не виключає жодних винятків. Дзвінок небезпечний

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


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

1
@cmaster Його, не очікуючи винятку,C() - це велика помилка. За замовчуванням у C ++ є те, що будь-яка функція може кинути - вона вимагає явного, noexcept(true)щоб сказати компілятору інакше. За відсутності цієї обіцянки програміст повинен припустити, що функція може кинути.
Sjoerd

1
@cmaster Це його власна німа вина і нічого спільного з автором C (). Безпека винятку - річ, він повинен про це дізнатися і слідувати за нею. Якщо він називає функцію, яку він не знає, є неісключенням, він повинен чортів добре впоратися з тим, що станеться, якщо це кине. Якщо він не потребує виключення, він повинен знайти реалізацію, яка є.
DeadMG

@Sjoerd і DeadMG: Хоча стандарт дозволяє будь-якій функції кинути виняток, це не означає, що проекти повинні допускати винятки у своєму коді. Якщо ви забороняєте винятки зі свого коду, так, як це здається керівництвом ОП, не потрібно робити програмування, безпечне для винятку. І якщо ви спробуєте переконати команду-лідера дозволити винятки в базовій коді, яка раніше була винятком, не буде винятком, C()функція буде порушена за наявності винятків. Це дійсно дуже нерозумно, це приречене призвести до низки проблем.
cmaster

@cmaster Йдеться про новий проект - дивіться перший коментар до прийнятої відповіді. Отже, немає існуючих функцій, які будуть порушені, як і немає існуючих функцій.
Sjoerd

3

Якщо ви будуєте критичну систему, врахуйте, дотримуйтесь порад вашої команди та не використовуйте винятки. Це Правило 208 АВ у стандартах кодування повітряних транспортних засобів C ++ компанії «Локхід Мартін» . З іншого боку, рекомендації MISRA C ++ мають дуже специфічні правила щодо того, коли винятки можна і не можна використовувати, якщо ви будуєте програмну систему, сумісну з MISRA.

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

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

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