викидання виключень з деструктора


257

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

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

Отже, моє запитання таке: якщо викидання з деструктора призводить до невизначеної поведінки, як ви обробляєте помилки, які виникають під час деструктора?

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

Очевидно, такі помилки рідкісні, але можливі.


36
"Два винятки одразу" - це відповідь на фондовий продаж, але це не ПРИЄДНА причина. Справжня причина полягає в тому, що виняток слід кинути, якщо і лише тоді, коли не можуть бути дотримані пост-умови функції. Пост-умова деструктора полягає в тому, що об'єкта більше не існує. Цього не може статися. Отже, будь-яка операція, що не спричиняє відмов, повинна бути названа окремим методом, перш ніж об'єкт вийде за межі (розумні функції зазвичай мають лише один шлях успіху).
шприц

29
@spraff: Чи знаєте ви, що те, що ви сказали, означає "викинути RAII"?
Кос

16
@spraff: необхідність викликати "окремий метод, перш ніж об'єкт вийде за межі" (як ви писали) насправді викидає RAII! Код за допомогою таких об'єктів повинен забезпечити виклик такого методу до виклику деструктора .. Нарешті, ця ідея зовсім не допомагає.
Фрунсі

8
@Frunsi немає, тому що ця проблема випливає з того, що деструктор намагається зробити щось, що виходить за рамки простого звільнення ресурсів. Заманливо сказати "я завжди хочу закінчити робити XYZ", і думаю, що це аргумент для введення такої логіки в деструктор. Ні, не лінуйтеся, пишіть xyz()і зберігайте деструктор в чистоті від логіки, яка не є RAII.
шприц

6
@Frunsi Наприклад, зробити щось для файлу не обов'язково ОК, щоб зробити це в деструкторі класу, що представляє транзакцію. Якщо фіксація не вдалася, це занадто пізно впоратися з нею, коли весь код, який був задіяний в транзакції, вийшов із сфери застосування. Деструктор повинен відмовитися від транзакції, якщо не commit()буде викликаний метод.
Ніколас Вілсон

Відповіді:


197

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

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

Це в основному зводиться до:

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

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

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

Приклад:

std :: fstream

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

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

Скотт Майерс має чудову статтю з цього приводу у своїй книзі "Ефективний C ++"

Редагувати:

Мабуть, також у пункті "Ефективніший C ++",
пункт 11: Запобігайте виняткам із залишення руйнів


5
"Якщо ви не заперечуєте проти потенційного завершення програми, ви, ймовірно, повинні проковтнути помилку." - це, мабуть, має бути виняток (помилуйте каламбур), а не правило - тобто швидко провалюватися.
Ерік Форбс

15
Я не погоджуюсь. Завершення програми зупиняє розмотування стека. Більше деструктора більше не буде викликано. Усі відкриті ресурси залишатимуться відкритими. Я думаю, що ковтання виключення було б кращим варіантом.
Мартін Йорк

20
Очищення ОС очищає ресурси, це власник вимкнено. Пам'ять, FileHandles тощо. Що стосується складних ресурсів: з'єднання БД. Ця вихідна лінія до МКС, яку ви відкрили (автоматично пересилатиме тісні з'єднання)? Я впевнений, що NASA хотіла б, щоб ви чисто закрили з'єднання!
Мартін Йорк

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

2
@LokiAstari Транспортний протокол, який ви використовуєте для зв'язку з космічним кораблем, не може обробити перерване з'єднання? Ок ...
doug65536

54

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


1
Чи можете ви, будь ласка, пояснити, як викликали аборт () у вищенаведеній ситуації. Значить, контроль над виконанням все ще був у компілятора C ++
Крішна Оза,

1
@Krishna_Oza: Досить просто: кожен раз, коли помилка видається, код, який викликає помилку, перевіряє деякий біт, який вказує на те, що система виконання працює в процесі розмотування стека (тобто, обробляє якусь іншу, throwале ще не знайшла catchдля неї блок) у цьому випадку std::terminate(не abort) викликається замість підвищення (нового) винятку (або продовження розмотування стека).
Марк ван Левенен

53

Ми маємо тут розрізнятись, а не сліпо слідувати загальним порадам для конкретних випадків.

Зауважимо, що нижче ігнорується проблема контейнерів об'єктів і що робити перед обличчям декількох датрів об'єктів всередині контейнерів. (І це можна частково ігнорувати, оскільки деякі об'єкти просто не підходять для того, щоб помістити їх у контейнер.)

Про всю проблему стає простіше думати, коли ми розділимо класи на два типи. ДТЗ класу може мати дві різні обов'язки:

  • (R) випустити семантику (він же звільнить цю пам'ять)
  • (C) здійснити семантику (він же передає файл на диск)

Якщо ми розглянемо питання таким чином, то я вважаю, що можна стверджувати, що (R) семантика ніколи не повинна викликати виключення з dtor, оскільки є а) нічого, що ми можемо з цим зробити, і b) багато операцій з вільними ресурсами не роблять навіть передбачають перевірку помилок, наприклад .void free(void* p);

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

Якщо ми будемо слідувати маршруту RAII і дозволити для об'єктів, що мають (C) семантику у своїх d'tors, я думаю, що ми також повинні передбачити незвичайний випадок, коли такі d'tors можуть кидати. Звідси випливає, що не слід поміщати такі об'єкти в контейнери, а також випливає, що програма все ще може виконувати, terminate()якщо командний передавач кидає, тоді як інший виняток активний.


Що стосується поводження з помилками (семантика фіксування / відмовлення) та винятків, то є хороший розмову одного Андрія Олександреску : Поводження з помилками в C ++ / Декларативний потік управління (проведено в NDC 2014 )

У деталях він пояснює, як бібліотека Folly реалізує інструмент UncaughtExceptionCounterдля їх створення ScopeGuard.

(Слід зазначити , що інші теж були подібні ідеї.)

Хоча розмова не зосереджена на викиданні з дтор, вона показує інструмент, який можна використати сьогодні для позбавлення від проблем, коли кинути з дтор.

В майбутньому , там може бути станд функція для цього, див N3614 , і обговорення про це .

Upd '17: 17-й std::uncaught_exceptionsкращою особливістю C ++ для цього є afaikt. Я швидко процитую статтю cppref:

Примітки

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


6
Дуже згоден. І додавання ще однієї семантичної (Ro) відкатної семантики. Використовується зазвичай в охороні області. Як у випадку з моїм проектом, де я визначив макрос ON_SCOPE_EXIT. Справа про семантику відката полягає в тому, що тут може статися щось значиме. Тож ми дійсно не повинні ігнорувати провал.
Weipeng L

Я відчуваю, що єдиною причиною, коли ми робимо семантику в деструкторах, є те, що C ++ не підтримує finally.
користувач541686

@Mehrdad: finally є dtor. Це завжди називається, незважаючи ні на що. Для синтаксичного наближення, нарешті, перегляньте різні реалізації obsegu_guard. На сьогоднішній день, якщо техніка на місці (навіть у стандартній, це C ++ 14?) Виявляє, чи дозволено кидати дтор, це навіть може бути абсолютно безпечним.
Мартін Ба

1
@MartinBa: Я думаю, ви пропустили пункт мого коментаря, що дивно, оскільки я погодився з вашим уявленням, що (R) і (C) різні. Я намагався сказати, що dtor по суті є інструментом для (R) і finallyпо суті є інструментом для (C). Якщо ви не бачите чому: подумайте, чому виправдано кидати винятки один на одного в finallyблоки, і чому це не стосується деструкторів. (У певному сенсі це річ з даними та контролем . Деструктори - це випуск даних, finallyце звільнення контролю. Вони різні; шкода, що C ++ пов'язує їх між собою.)
користувач541686

1
@Mehrdad: Занадто довго тут. Якщо ви хочете, ви можете створити свої аргументи тут: programmers.stackexchange.com/questions/304067/… . Дякую.
Мартін Ба

21

Справжнє запитання, яке потрібно задати собі щодо кидання з деструктора, - "Що з цим може зробити покликаний?" Чи є насправді щось корисне, що ви можете зробити за винятком, що компенсувало б небезпеку, спричинену кидком із деструктора?

Якщо я знищую Fooоб'єкт, а Fooдеструктор викидає виняток, що я можу з ним розумно зробити? Я можу ввійти в систему, або можу проігнорувати. Це все. Я не можу "виправити" це, тому що Fooоб'єкт уже зник. У кращому випадку я реєструю виняток і продовжую так, ніби нічого не сталося (або припиняю програму). Чи справді варто потенційно викликати невизначену поведінку, кидаючи з деструктора?


11
Щойно помітив ... кидання з дтор ніколи не визначає поведінку. Звичайно, це може викликати термін (), але це дуже добре вказана поведінка.
Мартін Ба

4
std::ofstreamдеструктор змивається, а потім закриває файл. Під час промивання може виникнути повна помилка диска, з якою ви можете абсолютно зробити щось корисне: покажіть користувачеві діалогове вікно про помилку, сказавши, що диск не має вільного місця.
Енді

13

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

Що ви повинні запитати, є в цій ситуації

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

Що має наздогнати виняток? Чи повинен абонент, який телефонує? Або foo має це впоратися? Чому абонент foo повинен дбати про якийсь внутрішній об'єкт foo? Можливо, існує спосіб, яким мова визначає це, щоб мати сенс, але це буде нечитабельним і важким для розуміння.

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

Тоді розглянемо цей випадок

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

Коли видалення obj3 не вдається, як я фактично видаляю таким чином, що гарантовано не вийде з ладу? Це моя чорт пам'яті!

Тепер розглянемо в першому фрагменті коду об'єкт автоматично відходить, тому що його знаходиться в стеці, а Object3 знаходиться в купі. Оскільки вказівник на Object3 зник, ви начебто SOL. У вас витік пам'яті.

Тепер один безпечний спосіб зробити це наступний

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

Також дивіться це поширене запитання


Повторюючи цю відповідь, повторіть: перший приклад, про int foo(), ви можете використовувати блок-функцію спробувати, щоб обернути всю функціональну частину в блок-пробування, включаючи лову руйнівників, якщо ви потурбувались про це. Все-таки не кращий підхід, але це річ.
tyree731

13

З чернетки ISO для C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

Тож деструктори, як правило, повинні виловлювати винятки, а не дозволяти їм поширюватися з деструктора.

3 Процес виклику деструкторів для автоматичних об'єктів, побудованих на шляху від пробного блоку до виразу викиду, називається "розмотування стека". [Примітка: Якщо деструктор, викликаний під час розмотування стека, виключається, за винятком, викликається std :: термінація (15.5.1). Тож деструктори, як правило, повинні виловлювати винятки, а не дозволяти їм поширюватися з деструктора. - кінцева примітка]


1
Не відповіли на запитання - ОП це вже знає.
Арафангіон

2
@Arafangion Я сумніваюся, що він знав про це (std :: закінчення називається), оскільки прийнята відповідь робила абсолютно той самий пункт.
лотхар

@Arafangion, як в деяких відповідях тут деякі люди згадували, що аборт () називається; Або так, що std :: закінчується по черзі викликає функцію abort ().
Крішна Оза

7

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


7

Я перебуваю в групі, яка вважає, що кидання в деструктор схеми "охопленого охоронця" є корисним у багатьох ситуаціях - особливо для одиничних тестів. Однак майте на увазі, що в C ++ 11 кидання деструктора призводить до виклику, std::terminateоскільки деструктори неявно зазначаються noexcept.

Анджей Кржеміньський має чудовий пост на тему деструкторів, які кидають:

Він вказує, що C ++ 11 має механізм змінити дефоктори за замовчуванням noexcept:

У C ++ 11 деструктор неявно вказаний як noexcept. Навіть якщо ви не додаєте специфікацій та визначите свій деструктор так:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

Компілятор все-таки невидимо додасть специфікацію noexceptвашому деструктору. А це означає, що момент, коли ваш деструктор кине виняток, std::terminateбуде викликаний, навіть якщо не було ситуації з подвійним виключенням. Якщо ви дійсно налаштовані дозволити вашим руйнівникам кидати, вам доведеться це чітко вказати; у вас є три варіанти:

  • Явно вкажіть деструктора як noexcept(false):
  • Наслідуйте свій клас від іншого класу, який вже визначає його деструктор як noexcept(false) .
  • Помістіть у свій клас нестатичний член даних, який вже визначає його деструктор як noexcept(false).

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


6

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

Наприклад:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};

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

5

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

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

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


1
Не визначена поведінка, а скоріше негайне припинення.
Марк ван Левенен

У стандарті йдеться про «невизначеність поведінки». Така поведінка часто припиняється, але це не завжди.
DJClayworth

Ні, читайте [крім.термініровать] у розділі Обробка винятків-> Спеціальні функції (що в моїй копії стандарту 15.5.1, але нумерація його, ймовірно, застаріла).
Марк ван Левенен

2

Питання: Отже, моє запитання таке: якщо викидання з деструктора призводить до невизначеної поведінки, як ви обробляєте помилки, які виникають під час деструктора?

A: Є кілька варіантів:

  1. Нехай винятки витікають з вашого деструктора, незалежно від того, що відбувається деінде. При цьому пам’ятайте (або навіть боязнь), що може слідувати std :: термінація.

  2. Ніколи не дозволяйте винятку витікати з вашого деструктора. Можливо, напишіть у журнал, якийсь великий червоний поганий текст, якщо зможете.

  3. моя фава : Якщо std::uncaught_exceptionповертається помилково, нехай випливають винятки. Якщо він повертає true, поверніться до підходу до журналу.

Але чи добре закидати дтор?

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

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


4
Твоя доля - це те, що я спробував нещодавно, і, виявляється, ти не повинен цього робити. gotw.ca/gotw/047.htm
GManNickG

1

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

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

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


1

На відміну від конструкторів, де кидання винятків може бути корисним способом вказати на те, що створення об’єкта вдалося, винятки не слід кидати в деструктори.

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

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


1
Запис повідомлення у файл журналу може спричинити виняток.
Конард

1

Мартін Ба (вгорі) знаходиться на правильному шляху - ви по-різному архітектору для логіки RELEASE та COMMIT.

Для випуску:

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

Для прихильності:

Тут вам потрібні такі ж об’єкти обгортки RAII, що такі речі, як std :: lock_guard, забезпечують mutexe. З тими, хто не вкладає логіку фіксації в dtor НА ВСІХ. У вас є спеціальний API для нього, потім обгорткові об'єкти, які будуть RAII здійснювати його у своїх ДІТЕРАХ та обробляти помилки там. Пам’ятайте, ви можете КЛЮЧИТИ винятки в деструкторі просто добре; її видача їх смертельна. Це також дозволяє реалізовувати політику та різні помилки, просто будуючи іншу обгортку (наприклад, std :: unique_lock vs. std :: lock_guard), і гарантує, що ви не забудете викликати логіку фіксації - це єдина половина шляху гідне виправдання для того, щоб поставити його в дтор на 1-му місці.


1

Отже, моє запитання таке: якщо викидання з деструктора призводить до невизначеної поведінки, як ви обробляєте помилки, які виникають під час деструктора?

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

Оскільки деструктори викликаються як для нормальних, так і для виняткових (невдалих) шляхів, вони самі не можуть вийти з ладу, інакше ми "не вдається провалитися".

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

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

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

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

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

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

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

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


1
Руйнування зараз невдача?
curiousguy

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

0

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

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