У вашому запитанні стверджується, що "Писати безпечний виняток код дуже важко". Я відповім спочатку на ваші запитання, а потім - на приховане запитання, що стоїть за ними.
Відповідаючи на запитання
Ви справді пишете безпечний код для виключення?
Звичайно, я.
Це причина Java втратив багато своєї привабливості для мене як програміста (відсутність RAII семантики) C ++, але я відволікся: Це C ++ питання.
Насправді це потрібно, коли вам потрібно працювати з STL або Boost кодом. Наприклад, потоки C ++ ( boost::thread
або std::thread
) викинуть виняток, щоб вийти вишукано.
Ви впевнені, що ваш останній код "готовий до виробництва" є безпечним для виключень?
Ви навіть можете бути впевнені, що це так?
Написання безпечного для винятку коду схоже на написання коду без помилок.
Ви не можете бути на 100% впевнені, що ваш код є безпечним для виключень. Але тоді ви прагнете до цього, використовуючи відомі візерунки та уникаючи відомих анти-шаблонів.
Чи знаєте ви та / або насправді використовуєте альтернативи, які працюють?
У C ++ немає життєздатних альтернатив (тобто вам потрібно буде повернутися назад до C та уникати бібліотек C ++, а також зовнішніх сюрпризів, таких як Windows SEH).
Написання безпечного коду виключення
Щоб написати безпечний код виключення, ви повинні спочатку знати, який рівень безпеки винятків має кожна інструкція, яку ви пишете.
Наприклад, викид new
може кинути виняток, але призначення вбудованого (наприклад, int чи покажчика) не вийде з ладу. Свід ніколи не вийде з ладу (ніколи не напишіть заміну кидок), std::list::push_back
може кинути ...
Гарантія винятку
Перше, що потрібно зрозуміти, це те, що ви повинні мати можливість оцінити гарантію виключення, яку пропонують усі ваші функції:
- жоден : Ваш код ніколи цього не повинен пропонувати. Цей код просочить усе і зламається при першому кинутому винятку.
- основні : Це гарантія, яку ви повинні принаймні запропонувати, тобто, якщо виняток буде викинуто, ресурси не просочуються, а всі об'єкти все ще цілі
- сильний : обробка буде або успішною, або викине виняток, але якщо вона кидає, то дані будуть у тому самому стані, як якщо б обробка не почалася взагалі (це дає транзакційну потужність на C ++)
- nothrow / nofail : обробка буде успішною.
Приклад коду
Наступний код здається правильним C ++, але по правді, він пропонує гарантію "жодного", і, таким чином, він невірний:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Я пишу на увазі весь свій код з таким аналізом.
Найнижча пропонована гарантія є базовою, але тоді, впорядкування кожної інструкції робить всю функцію "жодної", тому що якщо 3. кидає, x просочиться.
Перше, що потрібно зробити, було б зробити функцію "базовою", тобто поставити х у розумний вказівник, поки не буде надійно володіти списком:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Тепер наш код пропонує "основну" гарантію. Ніщо не просочиться, і всі об’єкти будуть у правильному стані. Але ми могли б запропонувати більше, тобто сильну гарантію. Ось це може дорого коштувати, і саме тому не всі C ++-коди є сильними. Давайте спробуємо:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Ми переупорядкували операції, спочатку створивши і встановивши X
правильне значення. Якщо будь-яка операція не вдається, вона t
не змінюється, тож операція 1-3 може вважатися "сильною": якщо щось кидає, t
не змінюється і X
не протікає, оскільки йому належить смарт-покажчик.
Потім ми створюємо копію t2
з t
, і працювати на цій копії від операції 4 до 7. Якщо що - то кидає, t2
модифікується, але потім, по- t
, як і раніше є оригінальним. Ми все ще надаємо сильну гарантію.
Потім обміняємо t
і t2
. Операції swap повинні бути нерозробленими на C ++, тому сподіваємось, що своп, про який ви писали, не T
є норовим (якщо його немає, перепишіть його, щоб він не був).
Отже, якщо ми дійшли до кінця функції, все вдалося (Не потрібно повертати тип) і t
має своє виняткове значення. Якщо вона не вдається, то t
все ще має своє початкове значення.
Тепер пропонувати гарантію може бути досить дорого, тому не прагніть надавати гарантію на весь ваш код, але якщо ви можете це зробити без витрат (а вбудовування C ++ та інша оптимізація можуть зробити весь код вище безцінним) , то зробіть це. Користувач функції вдячний вам за це.
Висновок
Писати безпечний для винятку код вимагає певної звички. Вам потрібно буде оцінити гарантію, яку надає кожна інструкція, яку ви використовуєте, а потім вам буде потрібно оцінити гарантію, запропоновану переліком інструкцій.
Звичайно, компілятор C ++ не створить резервну копію гарантії (в моєму коді я пропоную гарантію як тег доксігену @warning), що начебто сумно, але це не повинно заважати вам намагатися написати код, захищений від винятків.
Нормальний збій проти помилки
Як програміст може гарантувати, що функція без збоїв завжди буде успішною? Зрештою, у функції може бути помилка.
Це правда. Гарантії винятку повинні надаватися кодом без помилок. Але тоді, будь-якою мовою, виклик функції передбачає, що функція відсутня помилок. Жоден здоровий код не захищає себе від можливості появи помилки. Напишіть код найкращим чином, а потім запропонуйте гарантію з припущенням, що це відсутність помилок. А якщо помилка є, виправте її.
Винятки становлять винятковий збій в обробці, а не помилки коду.
Останні слова
Тепер питання "чи варто цього?".
Звичайно, так і є. Функція "nothrow / no-fail", знаючи, що функція не вийде з ладу - це велика користь. Те ж саме можна сказати і про "сильну" функцію, яка дозволяє писати код із трансакційною семантикою, як, наприклад, бази даних, з функціями фіксації / відкоту, при цьому комісія є звичайним виконанням коду, а винятки становлять відкат.
Тоді "базовий" - це найменша гарантія, яку ви повинні запропонувати. C ++ - це дуже сильна мова з його областями, що дозволяє вам уникнути будь-яких витоків ресурсів (те, що сміттєзбірнику важко буде запропонувати для баз даних, з'єднань або файлів).
Отже, наскільки я розумію, це є варто.
Редагувати 2010-01-29: Про некидальний своп
nobar зробив коментар, який, на мою думку, є досить актуальним, оскільки це частина "як ви пишете безпечний код виключення":
- [мені] своп ніколи не вийде з ладу (навіть не пишіть своп кидок)
- [nobar] Це хороша рекомендація для
swap()
функцій, написаних на замовлення . Слід зазначити, однак, що це std::swap()
може бути невдалим на основі операцій, які він використовує внутрішньо
за замовчуванням std::swap
буде зроблено копії та завдання, які для деяких об'єктів можуть кинутись. Таким чином, своп за замовчуванням міг би бути кинутим, або він використовується для ваших класів, або навіть для класів STL. Що стосується стандарту C ++, то операція swap для vector
, deque
і list
не кидає, тоді як це може бути, map
якщо функція порівняння може кинутись на конструювання копій (Див . C ++ Мова програмування, Спеціальне видання, додаток E, E.4.3 .Поміняти ).
Дивлячись на реалізацію свопу Visual C ++ 2008, його своп не кине, якщо два вектори мають однаковий розподільник (тобто звичайний випадок), але зробить копії, якщо вони мають різні розподільники. І, таким чином, я припускаю, що це може кинути цю останню справу.
Отже, оригінальний текст все ще зберігається: Ніколи не пишіть заміну кидок, але коментар nobar повинен пам’ятати: Будьте впевнені, що об’єкти, які ви міняєте, мають некидальний своп.
Редагувати 2011-11-06: Цікава стаття
Дейв Ейбрахамс , який дав нам основні / сильні / невикористані гарантії , описав у статті свій досвід щодо безпечного виключення STL:
http://www.boost.org/community/exception_safety.html
Подивіться на 7-й пункт (Автоматизоване тестування на виняток-безпека), де він покладається на автоматичне тестування одиниць, щоб переконатися, що кожен випадок перевірений. Я здогадуюсь, що ця частина є чудовою відповіддю на запитання автора " Чи можете ви навіть бути впевнені, що це так? ".
Редагувати 31.05.2013: Коментар від dionadar
t.integer += 1;
без гарантії, що переповнення не відбудеться НЕ виняток безпечно, і фактично може технічно викликати UB! (Переповнений підпис під UB: C ++ 11 5/4 "Якщо під час оцінки виразу результат математично не визначений або не знаходиться в діапазоні репрезентативних значень для його типу, поведінка не визначена". цілі числа не переповнюються, але виконують свої обчислення в класі еквівалентності по модулю 2 ^ # біт.
Діонадар має на увазі наступний рядок, який справді має невизначену поведінку.
t.integer += 1 ; // 1. nothrow/nofail
Рішення тут полягає в тому, щоб перевірити, чи ціле число вже на своєму максимальному значенні (використовуючи std::numeric_limits<T>::max()
), перш ніж робити додавання.
Моя помилка піде в розділі "Нормальний збій проти помилки", тобто помилка. Це не скасовує міркування, і це не означає, що безпечний для винятків код марний, оскільки неможливо досягти. Ви не можете захистити себе від вимкнення комп’ютера, від помилок компілятора, навіть від ваших помилок чи інших помилок. Ви не можете досягти досконалості, але можете спробувати наблизитися якомога ближче.
Я виправив код на увазі коментаря Діонадара.