Почему в C ++ нет конструкции finally?

57

Обработка исключений в C ++ ограничена попыткой / throw / catch. В отличие от Object Pascal, Java, C # и Python, даже в C ++ 11 finallyконструкция не была реализована.

Я видел очень много литературы по С ++, обсуждающей «код, исключающий исключение». Липпман пишет, что безопасный код исключений - это важная, но сложная и трудная тема, выходящая за рамки его учебника, из которого следует, что безопасный код не является фундаментальным для C ++. Херб Саттер посвящает 10 глав этой теме в своем «Исключительном C ++»!

Тем не менее, мне кажется, что многие проблемы, возникающие при попытке написать «безопасный код для исключения», могли бы быть достаточно хорошо решены, если бы finallyбыла реализована конструкция, позволяющая программисту гарантировать, что даже в случае исключения программа может быть восстановлена. в безопасное, стабильное состояние без утечек, близкое к точке выделения ресурсов и потенциально проблемного кода. Как очень опытный программист на Delphi и C #, я использую try ... finally довольно широко блокирует мой код, как и большинство программистов на этих языках.

Рассматривая все «навороты», реализованные в C ++ 11, я был удивлен, обнаружив, что «наконец-то» все еще не было.

Итак, почему finallyконструкция никогда не была реализована в C ++? Это действительно не очень сложная или продвинутая концепция для понимания, и она помогает программисту писать «безопасный для исключений код».

Вектор
источник
25
Почему нет наконец? Потому что вы освобождаете вещи в деструкторе, который срабатывает автоматически, когда объект (или умный указатель) покидает область видимости. Деструкторы превосходят finally {}, поскольку он отделяет рабочий процесс от логики очистки. Точно так же, как вы не хотите, чтобы вызовы free () загромождали рабочий процесс на языке сборки мусора.
mike30
2
Смотрите также Разработчики Java сознательно отказались от RAII?
BlueRaja - Дэнни Пфлугхофт
8
На вопрос «Почему нет finallyC ++, и какие методы обработки исключений используются вместо него?» действительно и по теме для этого сайта. Думаю, существующие ответы хорошо это освещают. Превращая это в дискуссию на тему «Есть ли причины, по которым дизайнеры C ++ не включают в себя finallyсмысл?» и "Должен finallyбыть добавлен в C ++?" и ведение дискуссии через комментарии по вопросу, и каждый ответ не соответствует модели этого сайта вопросов и ответов.
Джош Келли
2
Если у вас наконец получилось, у вас уже есть разделение задач: основной блок кода уже здесь, и проблема очистки решается здесь.
Каз
2
@Kaz. Разница неявная против явной очистки. Деструктор дает вам автоматическую очистку подобно тому, как простой старый примитив очищается, когда он выпадает из стека. Вам не нужно делать какие-либо явные вызовы для очистки и вы можете сосредоточиться на вашей основной логике. Представьте себе, насколько сложным было бы, если бы вы пытались очистить выделенные в стеке примитивы в попытке / наконец. Неявная очистка выше. Сравнение синтаксиса класса с анонимными функциями не имеет значения. Хотя, передав функции первого класса функции, которая освобождает дескриптор, можно централизовать ручную очистку.
mike30

Ответы:

57

В качестве дополнительного комментария к ответу @ Nemanja (который, поскольку он цитирует Страуструпа, на самом деле такой же хороший ответ, как вы можете получить):

Это просто вопрос понимания философии и идиом языка 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 ++ во многом зависит от такого рода оптимизаций». Написание лишнего кода, а затем оптимизация компилятора - это Хороший Дизайн ?! ИМО, это противоположность хорошего дизайна. Основой хорошего дизайна является лаконичный, легко читаемый код. Меньше для отладки, меньше для обслуживания и т. Д. И т. Д. Вы НЕ ДОЛЖНЫ писать куски кода, а затем полагаться на компилятор, чтобы все ушло - ИМО, который вообще не имеет смысла!
Вектор
14
@Mikey: То есть дублирование кода очистки (или тот факт, что очистка должна произойти) по всей базе кода является «кратким» и «легко читаемым»? С RAII вы пишете такой код один раз, и он автоматически применяется везде.
Mankarse
55

От Почему C ++ не предоставляет конструкцию "finally"? в FAQ по стилю и технике С ++ Бьярна Страуструпа :

Потому что C ++ поддерживает альтернативу, которая почти всегда лучше: техника «получение ресурсов - инициализация» (TC ++ PL3 раздел 14.4). Основная идея заключается в представлении ресурса локальным объектом, чтобы деструктор локального объекта освободил ресурс. Таким образом, программист не может забыть освободить ресурс.

Неманья Трифунович
источник
5
Но в этой технике нет ничего специфичного для C ++, не так ли? Вы можете сделать RAII на любом языке с объектами, конструкторами и деструкторами. Это отличная техника, но просто существующий RAII не означает, что finallyконструкция всегда бесполезна навсегда, несмотря на то , что говорит Струсуп. Тот факт, что написание «безопасного кода исключений» является большой проблемой в C ++, является доказательством этого. Черт, в C # есть и деструкторы finally, и они оба привыкли.
Такро
28
@Tacroy: C ++ - один из очень немногих основных языков, который имеет детерминированные деструкторы. C # «деструкторы» для этой цели бесполезны, и вам нужно вручную писать блоки «using», чтобы иметь RAII.
Неманя Трифунович
15
@ Слушай, у тебя есть ответ "Почему C ++ не предоставляет конструкцию" finally "?" непосредственно от самого Страуструпа. О чем еще ты можешь попросить? То есть почему.
5
@Mikey Если вы беспокоитесь о том, что ваш код ведет себя хорошо, в частности об отсутствии утечек ресурсов, когда в него вызываются исключения , вы беспокоитесь о безопасности исключений / пытаетесь написать код, безопасный для исключений. Вы просто так этого не называете, и из-за того, что доступны разные инструменты, вы реализуете это по-разному. Но это именно то, о чем говорят C ++, когда обсуждают безопасность исключений.
19
@Kaz: мне нужно только помнить, чтобы выполнить очистку в деструкторе один раз, и с тех пор я просто использую объект. Мне нужно помнить, чтобы выполнять очистку в блоке finally каждый раз, когда я использую выделяемую операцию.
deworde
19

Причина, по которой C ++ не имеет, finallyзаключается в том, что он не нужен в C ++. finallyиспользуется для выполнения некоторого кода независимо от того, произошло ли исключение или нет, что почти всегда является своего рода кодом очистки. В C ++ этот код очистки должен находиться в деструкторе соответствующего класса, и деструктор всегда будет вызываться, как finallyблок. Фраза использования деструктора для вашей очистки называется RAII .

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

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

Барт ван Инген Шенау
источник
2
Примечание: пожалуйста, не утверждайте, что C ++ имеет детерминированные деструкторы. Object Pascal / Delphi также имеет детерминированные деструкторы, но также поддерживает 'finally' по очень веским причинам, которые я объяснил в моих первых комментариях ниже.
Вектор
13
@Mikey: Учитывая, что никогда не было предложений о добавлении finallyв стандарт C ++, я думаю, можно с уверенностью сделать вывод, что сообщество C ++ не считает the absence of finallyпроблему. У большинства языков, в которых есть finallyC ++ , отсутствует последовательное детерминированное разрушение. Я вижу, что у Delphi есть их оба, но я не знаю его историю достаточно хорошо, чтобы знать, что было там первым.
Барт ван Инген Шенау
3
Dephi не поддерживает объекты на основе стека - только на основе кучи и ссылки на объекты в стеке. Следовательно, 'finally' необходимо для явного вызова деструкторов и т. Д., Когда это необходимо.
Вектор
2
В C ++ есть куча ошибок, которые, возможно, не нужны, так что это не может быть правильным ответом.
Каз
15
За более чем два десятилетия, когда я использовал этот язык и работал с другими людьми, которые использовали этот язык, я никогда не встречал работающего программиста на C ++, который говорил: «Я действительно хочу, чтобы у языка был finally». Я никогда не могу вспомнить ни одной задачи, которая бы облегчила бы, если бы у меня был к ней доступ.
Gort the Robot
12

Другие обсуждали RAII как решение. Это совершенно хорошее решение. Но на самом деле это не решает, почему они не добавили, finallyтак как это очень популярная вещь. Ответ на этот вопрос является более фундаментальным для проектирования и разработки C ++: на протяжении всей разработки C ++ те, кто участвовал в этом, решительно сопротивлялись внедрению конструктивных особенностей, которые могут быть достигнуты с использованием других функций без особой суеты, и особенно там, где это требует введения. новых ключевых слов, которые могут сделать старый код несовместимым. Поскольку RAII предоставляет высокофункциональную альтернативу, finallyи вы finallyв любом случае можете использовать свой собственный язык в C ++ 11, это было мало чем нужно.

Все, что вам нужно сделать, это создать класс, Finallyкоторый вызывает функцию, переданную ее конструктору в его деструкторе. Тогда вы можете сделать это:

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

    database.doRisky();
}

Однако большинство нативных программистов на C ++ предпочитают чисто спроектированные объекты RAII.

Джек Эйдли
источник
3
Вам не хватает захвата ссылки в вашей лямбде. Должно быть Finally atEnd([&] () { database.close(); });Кроме того , я представляю следующее лучше: { Finally atEnd(...); try {...} catch(e) {...} }(я поднял финализатор из-за примерки блока , так что выполняется после улова блоков.)
Томас Eding
2

Вы можете использовать шаблон «trap» - даже если вы не хотите использовать блок try / catch.

Поместите простой объект в требуемую область. В деструкторе этого объекта заложена ваша "финальная" логика. Независимо от того, что, когда стек размотан, будет вызван деструктор объекта, и вы получите свою конфету.

Арье Р
источник
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);
    });
}

Смотрите эту статью .

einpoklum - восстановить Монику
источник
-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 подходит для простых случаев, таких как управление памятью, но до тех пор, пока проблема исключений не будет решена, для решения общего назначения требуется слишком много размышлений / накладных расходов / беспокойства / перепроектирования.
Безумный ученый
3
Я понимаю вашу мысль - есть некоторые законные проблемы с деструкторами, которые могут выбрасывать - но они редки. Утверждение, что исключения RAII + имеют нерешенные проблемы или что RAII не является универсальным решением, просто не соответствует опыту большинства разработчиков C ++.
Джош Келли
1
Если вы обнаружите, что должны вызывать исключения в деструкторах, вы делаете что-то не так - возможно, используете указатели в других местах, когда они не нужны.
Вектор
1
Это слишком сложно для комментариев. Задайте вопрос об этом: как бы вы справились с этим сценарием в C ++, используя модель RAII ... он не работает ... Опять же, вы должны направить свои комментарии : введите @ и имя участника, с которым вы разговариваете в начале вашего комментария. Когда комментарии находятся на вашем собственном посте, вы получаете уведомление обо всем, но другие не делают, если вы не направите комментарий к ним.
Вектор