Ідіоматичне використання винятків у C ++


16

У isocpp.org виключення FAQ держави

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

З іншого боку, стандартна бібліотека визначає std :: logic_error та всі її похідні, які, як мені здається, мають обробляти, крім інших речей, помилки програмування. Чи передача порожнього рядка в std :: stof (викине invalid_argument) не помилка програмування? Чи передача рядка, що містить інші символи, ніж '1' / '0', до std :: bitset (буде кидати invalid_argument), не помилка програмування? Чи викликає std :: bitset :: set з недійсним індексом (викине_of_range) не помилка програмування? Якщо це не так, то що таке помилка програмування, яку випробували б? Конструктор на основі рядка std :: bitset існує лише з C ++ 11, тому він повинен бути розроблений з ідіоматичним використанням винятків на увазі. З іншого боку, у мене були люди, які говорять мені, що логіка_error в основному взагалі не повинна використовуватися.

Ще одне правило, яке часто зустрічається з винятками, - "використовувати виключення лише у виняткових обставинах". Але як функція бібліотеки повинна знати, які обставини є винятковими? Для деяких програм неможливість відкрити файл може бути винятковою. Для інших неможливість виділення пам'яті може бути не винятковою. І є 100 випадків між ними. Не можете створити розетку? Не вдається підключити або записати дані в сокет або файл? Не вдається проаналізувати вхід? Можливо, це буде винятковим, можливо, не буде. Сама функція, звичайно, не може знати, вона не має уявлення, в якому контексті вона викликається.

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


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

1
@RobertHarvey це визначення все ще має ту саму проблему - якщо щось можна вирішити без втручання людини чи ні, відомо лише верхнім шарам програми.
cooky451

1
Ви зациклюєтесь на юридичній справі. Оцініть плюси і мінуси і складіть свою думку. Також останній абзац у вашому запитанні ... Я взагалі не вважаю це само собою зрозумілим. Ваше мислення дуже чорно-біле, коли правда, напевно, ближче до кількох відтінків сірого.
Роберт Харві

4
Ви намагалися зробити якесь дослідження до того, як задати це питання? С ++ помилка в обробці ідіоми майже напевно обговорюється в нудотних деталях в Інтернеті. Одне посилання на один запис поширених запитань не є добрим дослідженням. Після того як ви зробите своє дослідження, вам все одно доведеться вирішити свою думку. Не запускайте мене з того, як наші школи програмування, очевидно, створюють безглузді програми програмного кодування, які не вміють думати самі.
Роберт Харві

2
Що підкреслює мою теорію про те, що таке правило може насправді не існувати. Я запросив декількох людей із The C ++ Lounge, щоб дізнатися, чи зможуть вони відповісти на ваше запитання, хоча кожен раз, коли я заходжу туди, їхня порада: "Перестаньте використовувати C ++, це обсмажить ваш мозок". Тож приймайте їхні поради на свій страх і ризик.
Роберт Харві

Відповіді:


15

По-перше, я відчуваю обов'язок зазначити, що std::exceptionі його діти були задумані давно. Є низка деталей, які, ймовірно, (майже напевно) відрізнялися, якби вони розроблялися сьогодні.

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

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

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

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

Щодо використання винятків для повідомлення про певні типи помилок: ви праві - та сама операція може бути кваліфікованою як виняток чи ні, залежно від способу її використання.

З іншого боку, ви також помиляєтесь - використання стандартної бібліотеки не обов'язково примушує це рішення до вас. У випадку відкриття файлу зазвичай використовується iostream. Iostreams теж не є найновішим і найкращим дизайном, але в цьому випадку вони все виходять правильно: вони дозволяють вам встановити режим помилок, тож ви можете контролювати, чи не вдалося відкрити файл, в результаті якого виняток буде кинуто чи ні. Отже, якщо у вас є файл, який дійсно необхідний для вашої програми, і якщо не вдалося відкрити, це означає, що вам доведеться вжити серйозних заходів щодо виправлення, то ви можете викинути виняток, якщо він не зможе відкрити цей файл. Для більшості файлів, які ви спробуєте відкрити, якщо вони не існують або недоступні, вони просто вийдуть з ладу (це за замовчуванням).

Що стосується того, як ви вирішите: я не думаю, що відповідь проста. На краще чи гірше, "виняткових обставин" не завжди легко виміряти. Хоча, безумовно, є випадки, які легко вирішити, вони повинні бути [не] винятковими, але є (і, мабуть, завжди, такі) випадки, коли це питання підлягає питанням або вимагає знання контексту, який знаходиться поза сферою функції. У подібних випадках може бути, принаймні, варто розглянути конструкцію, приблизно схожу з цією частиною iostreams, де користувач може вирішити, чи невдача призводить до викиду виключення чи ні. Крім того, цілком можливо мати два окремі набори функцій (або класи тощо), одна з яких видасть винятки для вказівки на відмову, а друга використовує інші засоби. Якщо ви йдете цим маршрутом,


9

Конструктор на основі рядка std :: bitset існує лише з C ++ 11, тому він повинен бути розроблений з ідіоматичним використанням винятків на увазі. З іншого боку, у мене були люди, які говорять мені, що логіка_error в основному не повинна використовуватися зовсім.

Ви можете не вірити в це, але, ну, різні кодери C ++ не згодні. Ось чому FAQ задає одне, але стандартна бібліотека не погоджується.

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

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

Можливо, це буде винятковим, можливо, не буде. Сама функція, звичайно, не може знати, вона не має уявлення, в якому контексті вона викликається.

Ідея тут полягає в тому, що якщо ваша функція не знає, виняткова чи ситуація, вона не повинна кидати виняток. Він повинен повернути стан помилки через якийсь інший механізм. Після досягнення точки в програмі, коли вона знає, що стан є винятковим, тоді він повинен кинути виняток.

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

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

Ви можете викинути винятки:

  1. Ніколи
  2. Скрізь
  3. Тільки на помилках програміста
  4. Ніколи на помилках програміста
  5. Тільки під час позапланових (виняткових) збоїв

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


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

1
@Jules - тепер це (продуктивність), безумовно, заслуговує на власну відповідь, де ви створюєте резервну копію своєї претензії. Виконання винятків для C ++ , безумовно, є проблемою, можливо, більше, можливо, менше, ніж деінде, але твердження про те, що "C ++ не є однією з цих мов [де винятки мають низьку ефективність]", безумовно, є дискусійним.
Мартін Ба

1
@MartinBa - порівняно, скажімо, з винятком Java, C ++, винятки на порядок швидші. Орієнтовні показники передбачають, що ефективність викидання винятків на 1 рівень приблизно на 50 разів повільніше, ніж обробка повернутого значення в C ++, порівняно з більш ніж 1000 разів повільніше в Java. Поради, написані для Java, у цьому випадку не слід застосовувати до C ++ без зайвих роздумів, оскільки між ними існує більше, ніж на порядок різниці в продуктивності. Можливо, я мав би написати «надзвичайно низьку продуктивність», а не «низьку продуктивність».
Жуль

1
@Jules - дякую за ці цифри. (будь-які джерела?) Я можу їм повірити, тому що Java (і C #) потребують фіксації сліду стека, що, безумовно, здається, що це може бути дуже дорого. Я все ще думаю, що ваша початкова відповідь начебто вводить в оману, тому що навіть 50-кратне уповільнення є досить важким, я думаю, особливо. мовою, орієнтованою на ефективність, як C ++.
Мартін Ба

2

Було написано багато інших хороших відповідей, я просто хочу додати короткий момент.

Традиційна відповідь, особливо коли був написаний FAQ C ISO +, в основному порівнює "Виняток C ++" та "Код повернення у стилі C". Третій варіант "повернути якийсь тип складеного значення, наприклад, structабо union, або сьогодні, boost::variantабо (запропонований) std::expected, не враховується.

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

ІМО, після C ++ 11, цей варіант "повернення дискримінаційного союзу", подібний до ідіоми, що Result<T, E>використовується сьогодні в Русті, слід частіше надавати перевагу коду С ++. Іноді це дійсно простіший і зручніший стиль вказівки на помилки. За винятком, завжди є така можливість, що функції, які раніше не кидалися, можуть раптово почати кидатись після рефактора, а програмісти не завжди так добре документують такі матеріали. Коли помилка вказується як частина повернутого значення в дискримінаційному об'єднанні, це значно зменшує ймовірність того, що програміст просто ігнорує код помилки, що є звичайною критикою обробки помилок у стилі C.

Зазвичай Result<T, E>працює на зразок підвищення як необов'язковий. Ви можете перевірити, використовуючи operator bool, чи це значення чи помилка. А потім використовуйте скажіть operator *для доступу до значення або іншої функції "отримати". Зазвичай цей доступ не є контрольованим, для швидкості. Але ви можете зробити так, що в налагодженній версії доступ перевіряється, і твердження гарантує, що насправді є значення, а не помилка. Таким чином, кожен, хто не належним чином перевірить помилки, отримає більш жорстке твердження, а не якусь більш підступну проблему.

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

Я став великим шанувальником цього стилю. Зазвичай я зараз використовую це або винятки. Але я намагаюся обмежити винятки великими проблемами. Щось на зразок помилки розбору, я намагаюся повернути, expected<T>наприклад. Такі речі, як std::stoiі boost::lexical_castякі викидають виняток C ++ у випадку якоїсь відносно незначної проблеми, "нитка не може бути перетворена на число", здається мені сьогодні дуже поганим смаком.


1
std::expectedвсе ще не прийнята пропозиція, правда?
Мартін Ба

Ви маєте рацію, я думаю, це ще не прийнято. Але є кілька реалізацій з відкритим кодом, що плавають навколо, і я кілька разів катав свою власну. Це менш складно, ніж робити тип варіанту, оскільки існує лише два можливі стани. Основні міркування щодо дизайну - це, який саме інтерфейс ви хочете, і чи хочете ви, щоб він виглядав як очікуваний Андреску <T> там, де насправді повинен бути об’єкт помилки exception_ptr, або ви просто хочете використовувати якийсь тип структури чи щось так як це.
Кріс Бек

Розмова Андрія Олександреску тут: channel9.msdn.com/Shows/Going+Deep/… Він детально показує, як побудувати такий клас та які міркування ви можете мати.
Кріс Бек

Запропонований варіант [[nodiscard]] attributeбуде корисним для цього підходу до вирішення помилок, оскільки він гарантує, що ви просто не ігноруєте результат помилки випадково.
CodesInChaos

- так, я знав розмови А.А. Я вважав дизайн досить дивним, оскільки для розпакування його ( except_ptr) вам довелося кинути виняток всередині. Особисто я вважаю, що такий інструмент повинен працювати абсолютно незалежно від виконання. Просто зауваження.
Мартін Ба

1

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

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

Якщо не придбати ресурси, то це найкращий кандидат для викидів винятків. Ресурсом може бути пам’ять, файл, підключення до мережі або будь-що інше, виходячи з вашої проблеми та платформи. Тепер, чи невдача до випуску ресурсу є винятком? Ну, і це знову залежить. Я нічого не робив, коли звільнити пам'ять не вдалося, тому не впевнений у тому сценарії. Однак, видалення файлів як частини випуску ресурсів може провалитись, і для мене не вдалося, і цей збій, як правило, пов'язаний з іншим процесом, тримаючи його відкритим у багатопроцесовій програмі. Я думаю, що інші ресурси можуть вийти з ладу під час випуску, як файл, і, як правило, ця проблема спричиняє цю проблему, тому виправити це було б краще, ніж кидання винятків.

Потім відбувається оновлення ресурсів. Цей пункт, принаймні, мені тісно пов'язаний з критичним аспектом роботи програми. Уявіть собі Employeeклас із функцією, UpdateDetails(std::string&)яка змінює деталі на основі заданого рядка, розділеного комами. Подібно до виходу з ладу пам’яті, мені важко уявити присвоєння значень змінних членів з ладом через відсутність досвіду в таких областях, де це може статися. Однак UpdateDetailsAndUpdateFile(std::string&)очікується , що функція, яка виконує вказівки на ім'я, не працює. Це я називаю критичною роботою.

Тепер ви маєте побачити, чи так звана критична операція вимагає відкидання виключення. Я маю на увазі, чи відбувається оновлення файлу наприкінці, як у деструкторі, чи це просто виклик параноїків після кожного оновлення? Чи існує механізм резервного копіювання, який регулярно записує неписані об'єкти? Що я говорю, ви повинні оцінити критичність операції.

Очевидно, що існує багато критичних операцій, які не пов'язані з ресурсом. Якщо UpdateDetails()дані надані неправильно, дані не оновлюватимуться, і про помилку потрібно повідомити, тому ви кинете тут виняток. Але уявіть собі подібну функцію GiveRaise(). Тепер, якщо вказаному працівникові пощастило мати гострого волосатого шефа і не отримає підвищення (з точки зору програмування, значення деякої змінної зупиняє це від того, що відбувається), функція по суті не працює. Ви б кинули тут виняток? Що я говорю, ви повинні оцінити необхідність виключення.

Для мене послідовність - це з точки зору мого підходу до дизайну, ніж зручності моїх занять. Що я маю на увазі, я не думаю, що з точки зору "всі функції Get повинні робити це, і всі функції" Оновлення повинні робити це ", але бачу, чи певна функція звертається до певної ідеї в рамках мого підходу. На його поверхні, класи можуть виглядати «випадково», але коли користувачі (в основному колеги з інших команд) дзвонять або запитують про це, я поясню, і вони здаються задоволеними.

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


1

По- перше, як уже говорилося, все не що ясна в C ++, ІМХО в основному тому , що вимоги і обмеження є кілька більш варіювалася в C ++ , ніж інші мови, особ. C # та Java, які мають "подібні" проблеми з винятками.

Викладу на прикладі std :: stof:

передача порожнього рядка в std :: stof (викине invalid_argument) не помилка програмування

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

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

Бічна примітка: З цього погляду на "a" runtime_error можна розглядати як на щось, що, з огляду на один і той же вхід до функції, теоретично може досягти успіху для різних прогонів. (наприклад, операція з файлами, доступ до БД тощо)

Наступна бічна примітка: Бібліотека регулярних виразів C ++ вирішила отримати помилку, runtime_errorхоча є випадки, коли її можна класифікувати так само, як і тут (невірний шаблон регулярного вираження).

Це просто показує, IMHO, що групування в помилки logic_або runtime_помилка є досить нечіткими в C ++ і насправді не сильно допомагає в загальному випадку (*) - якщо вам потрібно обробляти конкретні помилки, вам, ймовірно, потрібно ловитись нижче двох.

(*): Це не означає , що один шматок коди не повинен бути послідовним, але буде ви кинути runtime_або logic_або custom_щось дійсно не так уже й важливо, як мені здається.


Щоб коментувати і те, stofі bitset:

Обидві функції беруть рядки як аргумент, і в обох випадках це:

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

Правило, яке часто зустрічається з винятками, - "використовувати виключення лише у виняткових обставинах". Але як функція бібліотеки повинна знати, які обставини є винятковими?

Ця заява, IMHO, має два корені:

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

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

Приклад:

float readOrDefault;
try {
  readOrDefault = stof(...);
} catch(std::exception&) {
  // discard execption, just use default value
  readOrDefault = 3.14f; // 3.14 is the default value if cannot be read
}

Ось де такі функції, як TryParsevs. Parseвступають у дію: Одна версія для того, коли локальний код очікує, що розроблений рядок буде дійсним, та одна версія, коли локальний код передбачає, що насправді очікується (тобто не винятковий), що синтаксичний аналіз буде невдалим.

Дійсно, stofце просто (визначено як) обгортка навколо strtof, тому якщо ви не хочете винятків, використовуйте цю.


Отже, як я повинен вирішити, чи слід використовувати винятки чи ні для певної функції?

ІМХО, у вас є два випадки:

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

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

Звичайно, між ними знайдуться місця - просто використовуйте те, що потрібно, і пам’ятайте YAGNI.


Нарешті, я думаю, що я повинен повернутися до заяви FAQ,

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

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

Але коли це доречно, часто є специфічним для додатків, тому див. Вище домен бібліотеки проти домену додатка.

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

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