Чому в C ++ немає конструкції "нарешті"?


57

Поводження з винятками в C ++ обмежується спробою / кидати / ловити. На відміну від Object Pascal, Java, C # і Python, навіть у C ++ 11, finallyконструкція не була реалізована.

Я бачив дуже багато літератури на C ++, яка обговорювала "безпечний код для виключення". Ліппман пише, що безпечний код для виключення є важливою, але складною, важкою темою, що виходить за межі його Праймера - що, мабуть, означає, що безпечний код не є основним для C ++. Герб Саттер присвячує 10 глав цій темі у своєму винятковому С ++!

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

Розглядаючи всі "дзвіночки", застосовані в C ++ 11, я з подивом виявив, що "нарешті" все ще не було.

Отже, чому finallyконструкція ніколи не була реалізована в C ++? Це дійсно не дуже складна або передова концепція, щоб зрозуміти, і йде довгий шлях до допомоги програмісту написати «код безпечного виключення».


25
Чому нарешті ні? Тому що ви випускаєте речі в деструкторі, який автоматично запускається, коли об'єкт (або розумний вказівник) залишає область. Деструктори перевершують остаточно {}, оскільки він відокремлює робочий процес від логіки очищення. Так само, як ви б не хотіли, щоб дзвінки безкоштовно () захаращували робочий процес мовою, зібраною зі сміттям.
mike30


8
Задаючи питання: "Чому finallyв C ++ немає, і які методи для обробки виключень використовуються замість нього?" діє та на тему для цього сайту. Існуючі відповіді це добре висвітлюють, я думаю. Перетворюючи це на дискусію на тему: "Чи є дизайнери C ++ тим, що вони не включають в себе finallyкорисність?" та "Чи finallyслід додати до C ++?" і вести дискусію через коментарі до цього питання, і кожна відповідь не відповідає моделі цього веб-сайту з питань запитання.
Джош Келлі

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

2
@Kaz. Різниця неявна проти явного очищення. Деструктор дає вам автоматичне очищення, подібне до того, як очищається звичайний старий примітив, коли він вискакує зі стека. Вам не потрібно робити явні виклики очищення і можете зосередитись на вашій основній логіці. Уявіть, як би це було складно, якби вам довелося очистити стек від виділених примітивів у спробі / остаточно. Неявне очищення є вищим. Порівняння синтаксису класу з анонімними функціями не має значення. Хоча, передаючи функції першого класу функції, яка випускає ручку, може централізувати ручне очищення.
mike30

Відповіді:


57

Як деякий додатковий коментар до відповіді @ Nemanja (який, оскільки він цитує Stroustrup, насправді настільки ж хороший відповідь, як ви можете отримати):

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

Мовою, яка використовує try/ finally, код може виглядати приблизно так:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Простий і прямолінійний. Однак є кілька недоліків:

  • Якщо мова не має детермінованих деструкторів, я завжди повинен писати finallyблок, інакше я просочую ресурси.
  • Якщо DoRiskyOperationбільш ніж один виклик методу - якщо я маю зробити якусь обробку в tryблоці - то Closeоперація може закінчитися пристойним бітом від Openоперації. Я не можу написати прибирання поруч із моїм придбанням.
  • Якщо у мене є кілька ресурсів, які потрібно придбати, а потім звільнити безпечним для винятку способом, я можу закінчитись декількома шарами try/ finallyблоками.

Підхід C ++ виглядатиме так:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

Це повністю вирішує всі недоліки finallyпідходу. У нього є кілька своїх недоліків, але вони відносно незначні:

  • Є хороший шанс, що вам потрібно написати ScopedDatabaseConnectionклас самостійно. Однак це дуже проста реалізація - лише 4 або 5 рядків коду.
  • Це передбачає створення додаткової локальної змінної - яку ви, мабуть, не шануєте, грунтуючись на вашому коментарі про те, що "постійно створювати та знищувати класи, щоб покластися на їх деструкторів, щоб очистити ваш безлад дуже поганий", - але хороший компілятор оптимізує будь-яка додаткова робота, яка включає додаткову локальну змінну. Хороший дизайн C ++ багато в чому покладається на подібні оптимізації.

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

Нарешті, оскільки RAII є настільки усталеною ідіомою в C ++, і щоб звільнити розробників частину тягаря написання численних Scoped...класів, існують такі бібліотеки, як ScopeGuard та Boost.ScopeExit, які полегшують подібний спосіб детермінованого очищення.


8
У C # є usingоператор, який автоматично очищає будь-який об'єкт, що реалізує IDisposableінтерфейс. Тож як це можливо помилитися, виправити це досить легко.
Роберт Харві

18
Необхідно написати абсолютно новий клас, щоб подбати про перестановку тимчасової зміни стану, використовуючи дизайнерську ідіому, що реалізується компілятором з try/finallyконструктом, оскільки компілятор не try/finallyвідкриває конструкцію і єдиний спосіб отримати доступ до неї - це на основі класу дизайнерська ідіома, не є «перевагою»; це саме визначення інверсії абстракції.
Мейсон Уілер

15
@MasonWheeler - Гм, я не говорив, що писати новий клас - це перевага. Я сказав, що це недолік. Однак, на балансі, я віддаю перевагу RAII , ніж користуватися finally. Як я вже сказав, ваш пробіг може відрізнятися.
Джош Келлі

7
@JoshKelley: "Хороший дизайн C ++ багато в чому залежить від таких оптимізацій." Писати геб стороннього коду, а потім покладатися на оптимізацію компілятора - це гарний дизайн ?! IMO - це антитеза хорошого дизайну. Серед основ хорошого дизайну - стислий, легко читається код. Менше налагоджувати, менше підтримувати, і т. Д. Тощо. НЕ слід писати коди, а потім покладатися на компілятор, щоб все пройшло - ІМО, яке взагалі не має сенсу!
Вектор

14
@Mikey: Отже, дублювання коду очищення (або той факт, що очищення повинно відбуватися) у всій базі коду є "стислим" та "легко читабельним"? За допомогою RAII ви пишете такий код один раз, і він автоматично застосовується скрізь.
Манкарсе

55

З Чому C ++ не забезпечує "нарешті" конструкцію? у стилі та техніці FAQ Bjarne Stroustrup C ++ :

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


5
Але нічого немає в тій техніці, яка є специфічною для C ++? Ви можете робити RAII будь-якою мовою з об'єктами, конструкторами та деструкторами. Це чудова техніка, але просто існуючий RAII не означає, що finallyконструкція завжди марно вічно і назавжди, незважаючи на те, що говорить Strousup. Підтвердженням цього є сам факт того, що написання "безпечного коду для виключень" є великою справою на C ++. Чорт, C # має і деструктори finally, і вони обидва звикають.
Такрой

28
@Tacroy: C ++ - одна з небагатьох основних мов, яка має детерміновані деструктори. C # "деструктори" є марними для цієї мети, і вам потрібно вручну записати "використовуючи" блоки, щоб мати RAII.
Неманья Трифунович

15
@Mikey у вас є відповідь "Чому C ++ не забезпечує" нарешті "конструкцію?" безпосередньо від самого там Струструпа. Що ще можна попросити? Те є чому.

5
@Mikey Якщо ви турбуєтеся про вашому коді веде себе добре, зокрема , не відтік ресурсів, коли виключення кинуті на нього, ви будете турбуватися про безпеку винятків / намагаємося писати за виключення безпечного код. Ви просто не називаєте це так, і завдяки різним доступним інструментам ви реалізуєте його по-різному. Але це саме те, про що говорять люди на C ++, коли вони обговорюють безпеку виключень.

19
@Kaz: Мені потрібно лише пам’ятати, щоб зробити очищення в деструкторі один раз, і з цього моменту я просто використовую об’єкт. Мені потрібно пам’ятати, щоб робити очищення в остаточному блоці кожен раз, коли я використовую операцію, що виділяє.
deworde

19

Причина, якої немає у C ++, finallyполягає в тому, що вона не потрібна в C ++. finallyвикористовується для виконання деякого коду незалежно від того, стався виняток чи ні, який майже завжди є певним кодом очищення. У C ++ цей код очищення повинен бути в деструкторі відповідного класу, і деструктор завжди буде викликатися, як і finallyблок. Ідіома використання деструктора для очищення називається RAII .

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

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


2
Примітка. Будь ласка, не заперечуйте, що C ++ має детерміновані деструктори. У Object Pascal / Delphi також є детерміновані деструктори, але вони також підтримують "нарешті", з дуже хороших причин, які я пояснив у своїх перших коментарях нижче.
Вектор

13
@Mikey: З огляду на те, що ніколи не було пропозиції щодо доповнення finallyдо стандарту C ++, я вважаю, що можна зробити висновок, що спільнота C ++ не вважає the absence of finallyпроблемою. У більшості мов не finallyвистачає послідовного детермінованого знищення, яке має C ++. Я бачу, що у Delphi їх є обидва, але я не знаю її історії досить добре, щоб знати, хто там був першим.
Барт ван Іґен Шенау

3
Dephi не підтримує об'єкти на основі стека - лише на основі купи та посилання на об'єкти. Тому "нарешті" необхідно, коли це доречно, явно викликати деструктори тощо.
Вектор

2
У С ++ є чимало суворої частини, яка, напевно, не потрібна, тому це не може бути правильною відповіддю.
Каз

15
За більш ніж два десятиліття я використовував цю мову і працював з іншими людьми, які використовували цю мову, я ніколи не стикався з робочим програмістом на C ++, який сказав: "Я дуже хочу, щоб мова мала finally". Я ніколи не можу згадати жодне завдання, яке було б полегшеним, якби я мав до нього доступ.
Gort the Robot

12

Інші обговорили RAII як рішення. Це абсолютно вдале рішення. Але це насправді не стосується того, чому вони не додали finallyтак добре, оскільки це дуже бажана річ. Відповідь на це є більш важливим для проектування та розробки C ++: протягом усього розвитку C ++ ті, хто бере участь, наполегливо чинили опір впровадженню конструктивних особливостей, які можна досягти за допомогою інших функцій без величезної суєти, особливо коли це вимагає введення нових ключових слів, які можуть зробити старіший код несумісним. Оскільки RAII надає високофункціональну альтернативу, finallyі ви фактично можете передати свій власний finallyC ++ 11, заклик до нього мало.

Все, що вам потрібно зробити, це створити клас, Finallyякий викликає функцію, передану його конструктору, в його деструкторі. Тоді ви можете зробити це:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

Більшість нативних програмістів на C ++, як правило, віддадуть перевагу чітко розробленим об'єктам RAII.


3
У вашій лямбда відсутнє захоплення посилань. Finally atEnd([&] () { database.close(); });Також має бути краще, я вважаю, що краще: { Finally atEnd(...); try {...} catch(e) {...} }(Я підняв фіналізатор із
пробного

2

Ви можете використовувати шаблон "пастки" - навіть якщо ви не хочете використовувати блок "пробувати / ловити".

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


1
Це не дає відповіді на питання і просто доводить, що нарешті не така вже й погана ідея ...
Вектор

2

Що ж, ви можете сортувати власний finallyролик, використовуючи Lambdas, який отримав би наступне для складання штрафу (використовуючи приклад без RAII, звичайно, не найприємніший фрагмент коду):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Дивіться цю статтю .


-2

Я не впевнений, що згоден з твердженнями про те, що RAII - це супернабір finally. Ахілесова п'ята RAII проста: винятки. RAII реалізується з деструкторами, і викидати з деструктора завжди неправильно в C ++. Це означає, що ви не можете використовувати RAII, коли вам потрібен код очищення. Якби вони finallyбули реалізовані, з іншого боку, немає підстав вважати, що не було б законним викидати з finallyблоку.

Розглянемо такий шлях коду:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Якби ми мали, finallyми могли б написати:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Але я не можу знайти рівнозначну поведінку за допомогою RAII.

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


1
У вашому другому прикладі, якщо ви complex_cleanupможете кинути, то у вас може виникнути випадок, коли два льотні винятки є в польоті одразу, так само, як і з RAII / деструкторами, а C ++ відмовляється це дозволити. Якщо ви хочете бачити оригінальний виняток, тоді complex_cleanupслід запобігати будь-яким виняткам, як це було б і з RAII / деструкторами. Якщо ви хочете complex_cleanupпобачити виняток, то, я думаю, ви можете використовувати вкладені блоки try / catch - хоча це дотична і важко вписатися в коментар, тому варто окремого питання.
Джош Келлі

Я хочу використовувати RAII, щоб отримати ідентичну поведінку як перший приклад, більш безпечно. Кидок у передбачуваний finallyблок очевидно би працював так само, як кидок у catchблоці WRT винятків під час польоту - не викликати std::terminate. Питання "чому ні finallyв C ++?" а всі відповіді говорять "вам це не потрібно ... RAII FTW!" Моя думка полягає в тому, що так, RAII чудово підходить для простих випадків, таких як управління пам'яттю, але, поки питання про винятки не вирішено, потрібно занадто багато продумувати / накладні витрати / занепокоєння / переробляти, щоб бути загальним рішенням.
MadScientist

3
Я розумію вашу думку - є деякі законні проблеми з деструкторами, які можуть кинути, - але це рідко. Скажімо, що у винятках RAII + є невирішені проблеми або що RAII не є рішенням загального призначення, просто не відповідає досвіду більшості розробників C ++.
Джош Келлі

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

1
Це занадто залучено до коментарів. Залиште запитання щодо цього: Як би ви впоралися з цим сценарієм у C ++ за допомогою моделі RAII ... це, здається, не працює ... Знову ж вам слід направити свої коментарі : введіть @ та ім’я учасника, про який ви говорите до початку Вашого коментаря. Коли коментарі є у вашій власній публікації, ви отримуєте сповіщення про все, але інші - якщо ви не направляєте на них коментар.
вектор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.