У чому полягає концептуальна відмінність нарешті від деструктора?


12

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

Окрім питання, що finallyв C # та Java в основному можуть існувати лише один раз (== 1) на область дії, і одна область може мати декілька деструкторів C ++ (== n), я думаю, що вони по суті одне і те ж. (З деякими технічними відмінностями.)

Однак інший користувач стверджував :

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

Хтось може це очистити?

Відповіді:


6
  • Транзакція ( try)
  • Вихід / відповідь про помилку ( catch)
  • Зовнішня помилка ( throw)
  • Помилка програміста ( assert)
  • Відкат (найближчим моментом можуть бути охоронці сфери застосування на мовах, які підтримують їх на місцях)
  • Вивільнення ресурсів (деструктори)
  • Різний потік управління, незалежний від транзакцій ( finally)

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

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

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


4

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

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

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


4

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

У програмах c ++ програміст відповідає за вивільнення виділених ресурсів. Зазвичай це реалізується в деструкторі класу і робиться негайно, коли змінна виходить за межі області або коли викликається видалення.

Коли в c ++ створюється локальна змінна класу без використання newресурсів цих екземплярів, вивільнені неявним деструктором, коли є виняток.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

У java, c # та інших системах з автоматичним управлінням пам'яттю система збору сміття приймає рішення про знищення екземпляра класу.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

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

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}

Чому в C ++ чому деструктор викликається автоматично лише об'єктом стека, а не об'єктом купи в разі виключення?
Джорджіо

@Giorgio Оскільки накопичувальні ресурси живуть у просторі пам'яті, який не пов'язаний безпосередньо зі стеком викликів. Наприклад, уявіть багатопотокове додаток з 2 потоками Aта B. Якщо відкидається один потік, A'sтранзакція, що відкочується, не повинна знищувати виділені ресурси B, наприклад - стану потоків не залежать один від одного, і стійка пам'ять, що живе на купі, не залежить від обох. Однак, як правило, в C ++, пам'ять купи все ще прив’язана до об'єктів у стеку.

@Giorgio Наприклад, std::vectorоб'єкт може жити на стеку, але вказує на пам'ять на купі - і векторний об'єкт (у стеку), і його вміст (у купі) будуть розміщені під час розмотування стека в цьому випадку, оскільки знищення вектора в стеку викликало б деструктор, який звільняє пов'язану пам'ять на купі (і аналогічно знищує ці елементи купи). Як правило, для безпеки винятків більшість об'єктів C ++ живуть на стеці, навіть якщо вони обробляють лише вказівку на пам'ять на купі, автоматизуючи процес звільнення як купи, так і стека пам'яті під час розмотування стека.

4

Радий, що ви опублікували це як питання. :)

Я намагався сказати, що деструктори і finallyконцептуально різні:

  • Деструктори для випуску ресурсів ( даних )
  • finallyпризначений для повернення абоненту ( контроль )

Розглянемо, скажімо, цей гіпотетичний псевдокод:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyтут вирішується повністю проблема управління, а не проблема управління ресурсами.
Не було б сенсу робити це в деструкторі з різних причин:

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

Ось ще один приклад, цього разу, як у Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

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

З іншого боку, у цьому прикладі:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyзнищує ресурс, b. Це проблема даних. Проблема полягає не в чистому поверненні контролю абоненту, а в уникненні витоків ресурсів.
Відмова не є варіантом, і не повинен (концептуально) ніколи виникати.
Кожен реліз bобов'язково поєднується з придбанням, і є сенс використовувати RAII.

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


Дякую. Я не згоден, але так :-) Я думаю, що зможу додати ретельну відповідь протилежної точки зору в наступні дні ...
Мартін Ба

2
Як впливає на це той факт, який finallyвикористовується в основному для вивільнення ресурсів (не пам'яті)?
Барт ван Інген Шенау

1
@BartvanIngenSchenau: Я ніколи не стверджував, що будь-яка мова, яка існує зараз, має філософію чи реалізацію, що відповідає тому, що я описав. Люди ще не придумали все, що могло б існувати. Я лише стверджував, що в розділенні цих двох понять буде корисно, оскільки вони є різними ідеями та мають різні випадки використання. Щоб задовольнити вашу цікавість, я вважаю, що D має і те, і інше. Можливо, є й інші мови. Я не вважаю це актуальним, і мені було менше цікаво, чому, наприклад, виступає Java finally.
користувач541686

1
Практичний приклад, з яким я стикався в JavaScript, - це функції, які тимчасово змінюють вказівник миші на пісочний годинник протягом деякої тривалої операції (яка може кинути виняток), а потім повертають її до нормальної у finallyпункті. Світогляд C ++ вводить клас, який керує цим "ресурсом" призначення для псевдоглобальної змінної. Який поняттєвий сенс це має? Але деструктори - це молот C ++ для необхідного виконання коду в кінці блоку.
dan04

1
@ dan04: Велике спасибі, це ідеальний приклад для цього. Можу покластись, що я натрапив на стільки ситуацій, коли RAII не мав сенсу, але мені так важко було думати про них.
користувач541686

1

k3b відповідь дійсно добре це фразує:

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

Щодо "ресурсів", то я хотів би посилатися на Джона Калба: RAII повинен означати, що Придбання відповідальності - це ініціалізація .

У будь-якому випадку, що стосується неявних та явних, то, здається, це насправді:

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

Я думаю, що це все для концептуальної частини, ...


... зараз є ІМХО кілька цікавих деталей:

Я також не думаю, що c'tor / d'tor потрібно концептуально «придбати» або «створити» що-небудь, крім відповідальності за виконання деякого коду в деструкторі. Що, нарешті, також робить: запустіть якийсь код.

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

(Плюс, я зовсім не переконаний, що "добрий" код повинен вийти зрештою - можливо, це ще одне ціле питання до себе.)


Які ваші думки щодо мого прикладу Javascript?
користувач541686

Щодо інших ваших аргументів: "Чи справді ми хотіли б реєструвати те саме, що було безпомилковим?" Так, це лише приклад, і ви начебто не вистачаєте пункту, і так, ніхто ніколи не забороняв вносити більш конкретні деталі для кожного випадку. Сенс у тому, що ви, звичайно, не можете стверджувати, що ніколи не буває ситуації, в якій ви хочете записати щось спільне для обох. Деякі записи журналу є загальними, деякі - специфічними; ти хочеш обох. І знову ж таки, вам начебто зовсім не вистачає точки, зосереджуючись на реєстрації. Мотивація 10-рядкових прикладів є важкою; будь ласка, спробуйте не пропустити суть.
користувач541686

Ви ніколи не зверталися до цих ...
user541686

@Mehrdad - Я не звертався до вашого прикладу javascript, тому що це займе мені ще одну сторінку, щоб обговорити, що я думаю про це. (Я намагався, але мені знадобилося так довго, щоб сформулювати щось цілісне, що я пропустив це :-)
Мартін Бала

@Mehrdad - що стосується інших ваших моментів, - здається, ми маємо згоду погодитися. Я бачу, куди ти цілишся з різницею, але я просто не переконаний, що вони є чимось концептуально різним: головним чином тому, що я здебільшого в таборі, який думає, що кинути зрештою, є дуже поганою ідеєю ( зауважте : я також подумайте у своєму observerприкладі, що кидати там було б дуже поганою ідеєю.) Не соромтесь відкривати чат, якщо ви хочете обговорити це далі. Безумовно було весело думати про ваші аргументи. Ура.
Мартін Ба
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.