Здається, C ++ частіше використовує винятки.
Я б хотів би запропонувати фактично менше, ніж Objective-C, в деяких аспектах, тому що стандартна бібліотека C ++ взагалі не спричиняє помилок програміста, таких як вихід поза межі послідовності з випадковим доступом у його найбільш поширеній формі дизайну (у operator[], тобто) або намагаючись знешкодити недійсний ітератор. Мова не кидає доступ до масиву поза межами, або перенаправлення нульового вказівника, або що-небудь подібне.
Прийняття помилок програміста значною мірою з рівняння поводження з винятками фактично позбавляє дуже великої категорії помилок, на які часто відповідають інші мови throwing. C ++ має тенденцію до assert(який не збирається збиратись у випуску / виробництві, а лише виправляє налагодження) або просто виправляється (часто виходить з ладу) у таких випадках, ймовірно, частково, тому що мова не хоче стягувати вартість таких перевірок часу виконання як це потрібно для виявлення таких помилок програміста, якщо програміст спеціально не хоче оплатити витрати, написавши код, який сам проводить такі перевірки.
Sutter навіть рекомендує уникати винятків у таких випадках у стандартах кодування C ++:
Основним недоліком використання виключення для повідомлення про помилку програмування є те, що ви насправді не хочете, щоб розмотування стека виникало, коли ви хочете, щоб запуск налагоджувача запускався в точній рядку, де було виявлено порушення, при цьому стан лінії є неушкодженим. У підсумку: Є помилки, які, на вашу думку, можуть статися (див. Пункти 69 до 75). У всьому іншому, що не повинно, і виною програміста, якщо він є, є assert.
Це правило не обов'язково встановлюється в камені. У деяких більш критичних для місії випадках може бути кращим використовувати, скажімо, обгортки та стандарт кодування, який рівномірно реєструє, де трапляються помилки програміста, і throwза наявності помилок програміста, як-от намагання визначити щось недійсне або отримати доступ до нього поза межами, оскільки не вдасться відновити їх у тих випадках, якщо програмне забезпечення має шанс. Але в цілому більш поширене використання мови, як правило, не сприяє помилкам програміста.
Зовнішні винятки
Там, де я бачу винятки, які найчастіше заохочуються в C ++ (за даними стандартного комітету, наприклад) - це "зовнішні винятки", як у несподіваному результаті в зовнішньому джерелі поза програмою. Прикладом є невдача пам'яті. Інша - не вдається відкрити критичний файл, необхідний для запуску програмного забезпечення. Ще одна помилка підключення до потрібного сервера. Інший - користувач, що замикає кнопку переривання, щоб скасувати операцію, звичайний шлях виконання якої очікує на успіх відсутності цього зовнішнього переривання. Усі ці речі знаходяться поза контролем безпосереднього програмного забезпечення та програмістів, які його написали. Вони несподівані результати із зовнішніх джерел, які заважають операції (яку справді слід розглядати як неподільну транзакцію в моїй книзі *) не вдатися до успіху.
Операції
Я часто закликаю дивитися на tryблок як на "транзакцію", оскільки транзакції мають бути успішними в цілому або провалюватися в цілому. Якщо ми намагаємось щось зробити, і це не вдається на півдорозі, то будь-які побічні ефекти / мутації, внесені до програмного стану, як правило, потрібно повернути назад, щоб повернути систему у дійсний стан, як ніби транзакція взагалі ніколи не виконувалася, так само, як RDBMS, яка не обробляє запит на півдорозі, не повинна загрожувати цілісності бази даних. Якщо ви мутуєте стан програми безпосередньо у зазначеній транзакції, вам слід "вимкнути" його при виявленні помилки (і тут охоронці області можуть бути корисні з RAII).
Набагато простішою альтернативою є не мутування вихідного стану програми; Ви можете вимкнути його копію, а потім, якщо це вдасться, поміняти її оригіналом (переконайтесь, що своп не може викинути) Якщо це не вдалося, відмовтеся від копії. Це також застосовується, навіть якщо ви не використовуєте винятки для обробки помилок взагалі. "Транзакційний" спосіб мислення є ключовим для правильного відновлення, якщо мутації стану програми відбулися до того, як виникли помилки. Це або вдається в цілому, або провалюється в цілому. Зробити свої мутації це не вдалося на півдорозі.
Це химерно одна з найменш обговорюваних тем, коли я бачу програмістів, які запитують про те, як правильно робити помилки чи обробку винятків, але найскладніше з них усіх виправити будь-яке програмне забезпечення, яке хоче безпосередньо змінити стан програми у багатьох її операції. Чистота та незмінність тут можуть допомогти досягти безпеки виключень настільки ж, наскільки вони допомагають забезпечити безпеку ниток, оскільки мутація / зовнішній побічний ефект, який не виникає, не потребує відкочування.
Продуктивність
Іншим керівним фактором того, чи потрібно використовувати винятки чи ні, є ефективність, і я не маю на увазі нав'язливий, копійчастий, контрпродуктивний спосіб. Дуже багато компіляторів C ++ реалізують те, що називається "Обробка виключень з нульовою витратою".
Він пропонує нульові накладні витрати для виконання помилок, що перевершує навіть обробку помилок зі зворотним значенням C. Як компроміс, поширення винятку має великі витрати.
Згідно з тим, що я читав про це, це робить, що ваші загальні шляхи виконання випадків не вимагають накладних витрат (навіть не накладні витрати, які зазвичай супроводжують обробку коду помилок у стилі С) та в обмін на сильне перекручення витрат на виняткові шляхи ( а це означає throwing, що зараз дорожче, ніж будь-коли).
"Дороге" трохи важко оцінити, але, для початку, ви, мабуть, не хочете кидати мільйон разів у тугий цикл. Цей вид конструкції передбачає, що винятки не трапляються вліво та вправо весь час.
Непомилки
І ця продуктивність приводить мене до не помилок, що дивно нечітко, якщо ми подивимось на всілякі інші мови. Але я б сказав, враховуючи вищезгадану конструкцію EH з нульовою вартістю, що ви майже точно не хочете, щоб throwу відповідь на те, що ключ не знайдений у наборі. Тому що це не тільки помилка (людина, яка шукає ключ, можливо, побудувала набір і очікує, що шукає ключі, які не завжди існують), але це було б надзвичайно дорого в цьому контексті.
Наприклад, функція перетину набору може захотіти провести цикл через два набори та шукати спільні ключі. Якщо вам не вдалося знайти ключ threw, ви переглянете його і, можливо, зіткнуться з винятками в половині чи більше ітерацій:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
Цей приклад є абсолютно смішним і перебільшеним, але я бачив, що у виробничому коді деякі люди, що походять з інших мов, використовують винятки в C ++ дещо подібним чином, і я думаю, що це досить практичне твердження, що це не належне використання виключень. в C ++. Інший натяк вище - ви помітите, що catchблок абсолютно не має нічого спільного, і він просто написаний, щоб насильно ігнорувати будь-які подібні винятки, і зазвичай це натяк (хоча і не гарант), що винятки, ймовірно, не використовуються дуже належним чином у C ++.
Для таких типів випадків певний тип повернення значення, що вказує на збій (що завгодно, від повернення falseдо недійсного ітератора або що- nullptrнебудь має сенс у контексті), як правило, набагато доречніший, а також часто більш практичний і продуктивний, оскільки тип не помилки випадку зазвичай не вимагає, щоб якийсь процес розмотування стека дійшов до аналогічного catchмісця.
Запитання
Мені доведеться піти з внутрішніми прапорами помилок, якщо я вирішу уникати винятків. Чи буде це занадто сильно турбуватися, чи це, можливо, буде працювати навіть краще, ніж винятки? Порівняння обох випадків було б найкращою відповіддю.
Уникнення прямого винятку в C ++ мені здається надзвичайно контрпродуктивним, якщо ви не працюєте в якійсь вбудованій системі чи конкретному типі справ, який забороняє їх використання (у цьому випадку вам також доведеться вийти зі свого шляху, щоб уникнути всіх функціональність бібліотеки та мови, яка б інакше throw, як би суворо використовується nothrow new).
Якщо ви абсолютно повинні уникати винятків з будь-якої причини (наприклад, працюючи через межі API API модуля, з якого ви експортуєте C API), багато хто може зі мною не погодитися, але я б фактично запропонував використовувати глобальний обробник помилок / статус, як OpenGL glGetError(). Ви можете змусити його використовувати локальне сховище для потоку, щоб мати унікальний стан помилки на потік.
Моє обґрунтування цього полягає в тому, що я не звик бачити команди у виробничих середовищах ретельно перевіряти наявність усіх можливих помилок, на жаль, коли повертаються коди помилок. Якби вони були ретельними, деякі C-API можуть зіткнутися з помилкою практично при кожному виклику API C, і ретельна перевірка потребує чогось типу:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... майже кожен рядок коду викликає API, який вимагає таких перевірок. Та все ж мені не вистачало щастя співпрацювати з командами. Вони часто ігнорують такі помилки половину, іноді навіть більшість часу. Це найбільше звернення до мене винятків. Якщо ми перетворимо цей API і зробимо його рівномірно, коли throwвиникне помилка, виняток неможливо ігнорувати , і на мій погляд, і досвід, саме тут лежить перевагу винятків.
Але якщо винятки неможливо використати, то глобальний статус помилки на кожну нитку має принаймні перевагу (величезний порівняно з поверненням мені кодів помилок), що він може мати шанс зафіксувати колишню помилку трохи пізніше, ніж коли траплявся в якійсь неохайній кодовій базі замість того, щоб відверто пропустити її, і залишив нас зовсім забути про те, що сталося. Помилка могла статися за кілька рядків до або в попередньому виклику функції, але за умови, що програмне забезпечення ще не вийшло з ладу, ми можемо почати працювати назад і з'ясувати, де і чому вона сталася.
Мені здається, оскільки вказівники рідкісні, мені доведеться переходити з внутрішніми прапорами помилок, якщо я вирішу уникати винятків.
Я б не обов'язково говорив, що вказівники рідкісні. В C ++ 11 і далі існують навіть методи, щоб знайти основні вказівники даних контейнерів та нове nullptrключове слово. Зазвичай вважається нерозумним використовувати сировинні покажчики для володіння / управління пам’яттю, якщо ви можете використовувати щось на зразок, unique_ptrвраховуючи, наскільки важливим є відповідність RAII за наявності винятків. Але необроблені покажчики, які не володіють пам’яттю та керують нею, не обов'язково вважаються настільки поганими (навіть від таких людей, як Саттер та Струструп), а іноді дуже практичні як спосіб вказувати на речі (поряд з показниками, які вказують на речі).
Вони, мабуть, не менш безпечні, ніж стандартні ітератори контейнерів (принаймні у випуску, відсутні перевірені ітератори), які не виявлятимуть, якщо ви спробуєте знеструмити їх після їх недійсності. Я б сказав, що C ++ все ще є безсоромно небезпечною мовою, якщо тільки ваше конкретне використання не захоче все приховати і приховати навіть неіснуючі вказівки. З винятками майже критично, що ресурси відповідають RAII (який, як правило, не витрачається на час виконання), але крім того, що це не обов'язково намагатися бути найбезпечнішою мовою для використання на користь уникнення витрат, які розробник не хоче прямо обмін на щось інше. Рекомендоване використання не намагається захистити вас від таких речей, як звисаючі покажчики та недійсні ітератори, так би мовити (інакше нас рекомендують використовуватиshared_ptrповсюдно, що Stroustrup гостро протистоїть). Він намагається захистити вас від невдачі належним чином звільнити / випустити / знищити / розблокувати / очистити ресурс, коли щось throws.