Существует ли предпочтительная идиома для имитации Java try / finally в C ++?

StackOverflow https://stackoverflow.com/questions/500244

Вопрос

Занимаюсь Java уже несколько лет, поэтому не отслеживал C ++.Имеет наконец - то предложение было добавлено в обработку исключений C ++ в определении языка?

Есть ли любимая идиома, которая имитирует Java try / finally?

Меня также беспокоит, что C ++ не имеет окончательного супертипа для всех возможных исключений, которые могли бы быть сгенерированы - как класс Throwable в Java.

Я могу писать:

try {
  // do something
} catch(...) {
  // alas, can't examine the exception
  // can only do cleanup code and perhaps rethrow, ala:
  throw;
}

ДОБАВЛЕНИЕ РЕДАКТИРОВАТЬ:

В итоге я принял ответ, который набрал наибольшее количество голосов, т. е. использовал деструкторы для очистки.Конечно, из моих собственных комментариев ясно, что я не совсем согласен с этим.Однако C ++ - это то, что он есть, и поэтому в прикладных усилиях, которые я имею в виду я собираюсь более или менее стремиться придерживаться общего сообщества практика.Я буду использовать шаблонные классы, чтобы обернуть ресурсы, у которых еще нет деструктора класса (т. Е. библиотеки C ресурсов), таким образом наделяя их семантикой деструктора.

НОВОЕ ДОПОЛНЕНИЕ РЕДАКТИРОВАТЬ:

Хм, вместо наконец - то затем завершение возможно, особенность?Закрытие в сочетании с подходом ScopeGuard (см. Один из ответов ниже) было бы способом выполнить очистку с помощью произвольных действий и доступа к очистке контекст внешней области кода.Очистка может быть выполнена в стиле идиомы, который используется в программировании на Ruby, где они предоставляют блоки очистки при открытии ресурса.Разве функция закрытия не рассматривается для C ++?

Это было полезно?

Решение

Эффективно используя деструкторы.Когда в блоке try генерируется исключение, любой объект, созданный в нем, будет немедленно уничтожен (и, следовательно, вызван его деструктор).

Это отличается от Java, где вы понятия не имеете, когда будет вызван финализатор объекта.

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

Другие советы

Мои 02 доллара.Я много лет программировал на управляемых языках, таких как C # и Java, но был вынужден перейти на C ++ в целях повышения скорости.Сначала я не мог поверить, что мне пришлось дважды выписывать сигнатуру метода в файле заголовка, а затем в файле cpp, и мне не понравилось, что не было блока finally, а отсутствие сборки мусора означало отслеживание утечек памяти повсюду - черт возьми, мне это совсем не понравилось!

Однако, как я уже сказал, я был вынужден использовать C ++.Так что я был вынужден серьезно изучить его, и теперь я, наконец, понял все идиомы программирования, такие как RAII, и я получаю все тонкости языка и тому подобное.Это заняло у меня некоторое время, но теперь я вижу, насколько этот язык отличается от C # или Java.

В наши дни я думаю, что C ++ - это лучший язык, который только существует!Да, я могу понять, что иногда встречается немного больше того, что я называю "мякиной" (казалось бы, ненужный материал для написания), но после того, как я действительно серьезно использовал язык, я полностью изменил свое мнение об этом.

Раньше у меня постоянно были утечки памяти.Раньше я записывал весь свой код в файл .h, потому что я ненавидел разделение кода, я не мог понять, зачем они это делают!И раньше я всегда заканчивал с дурацкими циклическими зависимостями включения и еще кучей других.Я действительно был зациклен на C # или Java, для меня C ++ был огромным шагом вниз.В эти дни я это понимаю.У меня почти никогда не бывает утечек памяти, мне нравится разделение интерфейса и реализации, и у меня больше нет проблем с зависимостями цикла.

И я тоже не пропускаю финальный блок.Честно говоря, мое мнение таково, что эти программисты на C ++, о которых вы говорите, пишущие повторяющиеся действия по очистке в блоках catch, просто кажутся мне плохими программистами на C ++.Я имею в виду, не похоже, что у кого-либо из других программистов на C ++ в этой теме возникают какие-либо проблемы, о которых вы упоминаете.RAII действительно делает finally избыточным, и, если уж на то пошло, это требует меньше работы.Вы пишете один деструктор, и тогда вам больше никогда не придется писать другой, наконец-то, никогда!Ну, по крайней мере, для этого типа.

При всем уважении, я думаю, что происходит то, что вы просто привыкли к Java сейчас, точно так же, как и я когда-то.

Ответ C ++ - RAII:Деструктор объекта будет выполнен, когда они выйдут за пределы области видимости.Будь то возврат, исключение или что-то еще.Если вы обработаете исключение где-то в другом месте, вы можете быть уверены, что все объекты от вызываемой функции до вашего обработчика будут должным образом уничтожены путем вызова их деструктора.Они уберут за вами.

Читать http://en.wikipedia.org/wiki/Resource_acquisition_is_initialization

No finally не был добавлен в C ++ и, скорее всего, никогда не будет добавлен.

То, как C ++ использует конструктор / деструктор, делает необходимость в finally ненужной.
Если вы используете catch (...) для очистки, значит, вы неправильно используете C ++.Весь код очистки должен быть в деструкторе.

Хотя его использование не является обязательным, в C ++ есть std::exception .
Принуждение разработчиков к производным от определенного класса для использования exception противоречит философии C ++ keep it simple.Именно поэтому мы не требуем, чтобы все классы были производными от Object .

Читать: Поддерживает ли C ++ блоки 'finally'?(И что это за "RAII", о котором я все время слышу?)

Использование finally более подвержено ошибкам, чем деструкторы для очистки.
Это происходит потому, что вы заставляете пользователя объекта выполнять очистку, а не дизайнера / разработчика класса.

Хорошо, я должен добавить ответ на вопросы, которые вы высказали в отдельном сообщении с ответом:(Было бы намного удобнее, если бы вы отредактировали это в исходном вопросе, чтобы он не заканчивался внизу ниже ответы на него.

Если бы вся очистка всегда выполнялась в деструкторах, тогда не было бы необходимости содержать какой-либо код очистки в catch блоке - однако в C ++ есть блоки catch, в которых выполняются действия по очистке.Действительно, в нем есть блок для catch (...), где это возможно только для выполнения действий по очистке (ну, конечно, не удается получить ни от какой информации об исключении для выполнения каких-либо протоколирования).

catch имеет совершенно отдельную цель, и как Java-программист вы должны знать об этом.Предложение finally предназначено для "безусловных" действий по очистке.Независимо от того, как происходит выход из блока, это должно быть сделано.Catch предназначен для условной очистки.Если генерируется исключение такого типа, нам нужно выполнить несколько дополнительных действий.

Очистка в блоке finally будет выполнена независимо от того, было ли вызвано исключение или нет - что является тем, что всегда хочется, чтобы произошло, когда код очистки действительно существует.

Неужели?Если мы захотим, чтобы это всегда произойдет для этого типа (скажем, мы всегда хотим закрыть соединение с базой данных, когда закончим с ним), тогда почему бы нам не определить это однажды?В самом типе?Заставить соединение с базой данных закрыться само по себе, вместо того, чтобы пытаться / finally использовать его при каждом отдельном использовании?

В этом весь смысл деструкторов.Они гарантируют, что каждый тип способен самостоятельно выполнять очистку при каждом его использовании, не заставляя вызывающего пользователя думать об этом.

Разработчики C ++ с самого первого дня страдали от необходимости повторять очистку действия, которые появляются в блоках catch в потоке кода, который происходит после успешного выхода из блока try.Программисты Java и C # просто делают это один раз в блоке finally.

Нет.Программисты на C ++ никогда не страдали от этого.У программистов на Си есть.И программисты на C, которые поняли, что у c ++ есть классы, а затем назвали себя программистами на C ++, имеют.

Я программирую на C ++ и C # ежедневно, и я чувствую, что меня преследует нелепая настойчивость C # в том, что я должен указать предложение finally (или using блокировать) КАЖДЫЙ РАЗ, когда я использую подключение к базе данных или что-то еще, что необходимо очистить.

C ++ позволяет мне указать раз и навсегда, что "всякий раз, когда мы заканчиваем с этим типом, он должен выполнять эти действия".Я не рискую забыть освободить память.Я не рискую забыть закрыть дескрипторы файлов, сокеты или подключения к базе данных.Потому что моя память, мои дескрипторы, сокеты и подключения к БД делают это сами.

Как это может когда - либо было бы предпочтительнее писать повторяющийся код очистки каждый раз, когда вы используете тип?Если вам нужно обернуть тип, потому что у него нет самого деструктора, у вас есть два простых варианта:

  • Поищите подходящую библиотеку C ++, которая предоставляет этот деструктор (подсказка:Повышение)
  • Используйте boost::shared_ptr, чтобы обернуть его, и снабдите его пользовательским функтором во время выполнения, указав, какую очистку необходимо выполнить.

Когда вы пишете сервер приложений программное обеспечение, подобное серверам приложений Java EE Glassfish, JBoss и т.д., вы хотите иметь возможность перехватывать и регистрировать исключение информация - в отличие от того, чтобы позволить ей упасть на пол.Или, что еще хуже, попадают в среду выполнения и вызывают нелюбезный резкий выход сервера приложений.Вот почему очень желательно иметь всеобъемлющий базовый класс для любого возможного исключения.И в C ++ есть именно такой класс.std::исключение.

Занимался C ++ со времен CFront и Java / C # большую часть этого десятилетия.Является ясно видеть, что существует просто огромный культурный разрыв в том, насколько фундаментально подходят к подобным вещам.

Нет, вы никогда не занимались C ++.Вы выполнили CFront или C с классами.Не C ++.Это огромная разница.Перестаньте называть ответы неубедительными, и, возможно, вы узнаете кое-что о языке, который, как вам казалось, вы знаете.;)

Функции очистки сами по себе совершенно убогие.У них низкая сплоченность, поскольку ожидается, что они будут выполнять ряд действий, связанных только тогда, когда они произойдут.Они обладают высокой связью в том смысле, что им необходимо модифицировать свои внутренние компоненты, когда изменяются функции, которые на самом деле что-то делают.Из-за этого они подвержены ошибкам.

Конструкция try...finally - это фреймворк для функций очистки.Это поощряемый языком способ написания паршивого кода.Более того, поскольку это поощряет написание одного и того же кода очистки снова и снова, это подрывает принцип DRY.

Способ C ++ гораздо предпочтительнее для этих целей.Код очистки для ресурса записывается ровно один раз, в деструкторе.Он находится в том же месте, что и остальной код для этого ресурса, и поэтому обладает хорошей связностью.Код очистки не обязательно помещать в несвязанные модули, и, следовательно, это сокращает время связывания.Она написана ровно один раз, когда хорошо продумана.

Более того, способ C ++ гораздо более единообразен.C ++ с добавлениями интеллектуального указателя обрабатывает все виды ресурсов одинаковым образом, в то время как Java хорошо обрабатывает память и предоставляет неадекватные конструкции для освобождения других ресурсов.

С C ++ существует множество проблем, но это не одна из них.Есть способы, в которых Java лучше, чем C ++, но это не один из них.

Java было бы намного лучше использовать способ реализации RAII вместо того, чтобы пытаться ... наконец.

Чтобы избежать необходимости определять класс-оболочку для каждого выпускаемого ресурса, вас может заинтересовать ScopeGuard (http://www.ddj.com/cpp/184403758), который позволяет создавать "очистители" на лету.

Например:

FILE* fp = SomeExternalFunction();
// Will automatically call fclose(fp) when going out of scope
ScopeGuard file_guard = MakeGuard(fclose, fp);

Пример того, как трудно правильно использовать finally.

Открытие и закрытие двух файлов.
Где вы хотите гарантировать, что файл закрыт правильно.
Ожидание GC - это не вариант, так как файлы могут быть использованы повторно.

В C++

void foo()
{
    std::ifstream    data("plop");
    std::ofstream    output("plep");

    // DO STUFF
    // Files closed auto-magically
}

На языке без деструкторов, но с предложением finally.

void foo()
{
    File            data("plop");
    File            output("plep");

    try
    {
        // DO STUFF
    }
    finally
    {
        // Must guarantee that both files are closed.
        try {data.close();}  catch(Throwable e){/*Ignore*/}
        try {output.close();}catch(Throwable e){/*Ignore*/}
    }
}

Это простой пример, и код уже становится запутанным.Здесь мы только пытаемся мобилизовать 2 простых ресурса.Но по мере увеличения количества ресурсов, которыми необходимо управлять, и / или увеличения их сложности использование finally-блока становится все труднее и труднее корректно использовать при наличии исключений.

Использование finally перекладывает ответственность за правильное использование на пользователя объекта.Используя механизм конструктора / деструктора, предоставляемый C ++, вы перекладываете ответственность за правильное использование на разработчика / реализатора класса.Это наследственно безопаснее, поскольку разработчику нужно только один раз сделать это правильно на уровне класса (вместо того, чтобы разные пользователи пытались сделать это правильно разными способами).

Использование C ++ 11 с его лямбда - выражения, Недавно я начал использовать следующий код для имитации finally:

class FinallyGuard {
private:
  std::function<void()> f_;
public:
  FinallyGuard(std::function<void()> f) : f_(f) { }
  ~FinallyGuard() { f_(); }
};

void foo() {
  // Code before the try/finally goes here
  { // Open a new scope for the try/finally
    FinallyGuard signalEndGuard([&]{
      // Code for the finally block goes here
    });
    // Code for the try block goes here
  } // End scope, will call destructor of FinallyGuard
  // Code after the try/finally goes here
}

В FinallyGuard является объектом, который создается с вызываемым аргументом, подобным функции, предпочтительно лямбда-выражением.Он просто запомнит эту функцию до тех пор, пока не будет вызван ее деструктор, что происходит в том случае, когда объект выходит за пределы области видимости либо из-за обычного потока управления, либо из-за разматывания стека во время обработки исключений.В обоих случаях деструктор вызовет функцию, таким образом выполнив рассматриваемый код.

Немного странно, что вам приходится писать код для finally до того , как код для try блок, но в остальном это действительно очень похоже на подлинный try/finally с Java.Я думаю, не следует злоупотреблять этим в ситуациях, когда объект со своим собственным деструктором был бы более подходящим, но есть случаи, когда я считаю этот подход, описанный выше, более подходящим.Я обсуждал один такой сценарий в этот вопрос.

Насколько я понимаю ситуацию, std::function<void()> будет использовать некоторую косвенность указателя и по крайней мере один вызов виртуальной функции для выполнения своей удаление типа, так что будет накладные расходы на производительность.Не используйте этот метод в узком цикле, где производительность имеет решающее значение.В этих случаях более подходящим был бы специализированный объект, деструктор которого выполняет только одно действие.

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

Я думаю, что вы упускаете из виду суть того, что catch (...) могу сделать.

В своем примере вы говорите: "увы, не могу проверить исключение".Ну, у вас нет информации о типе исключения.Вы даже не знаете, является ли это полиморфным типом, поэтому, даже если бы у вас была какая-то нетипизированная ссылка на него, вы не смогли бы даже безопасно попытаться dynamic_cast.

Если вы знаете об определенных исключениях или иерархиях исключений, с которыми вы можете что-то сделать, то это место для блоков catch с явно обозначенными типами.

catch (...) не часто бывает полезен в C ++.Он может быть использован в местах, которые должны гарантировать, что они не выдают, или только выдают определенные сокращенные исключения.Если вы используете catch (...) что касается очистки, то есть очень хороший шанс, что ваш код в любом случае не является надежно защищенным от исключений.

Как упоминалось в других ответах, если вы используете локальные объекты для управления ресурсами (RAII), то может быть удивительно и поучительно, как мало блоков catch вам нужно, часто - если вам не нужно ничего делать локально с исключением - даже блок try может быть избыточным, поскольку вы позволяете исключениям поступать в клиентский код, который может реагировать на них, по-прежнему гарантируя отсутствие проблем с ресурсами.

Чтобы ответить на ваш первоначальный вопрос, если вам нужен какой-то фрагмент кода для запуска в конце блока, с исключением или без исключения, то рецептом будет.

class LocalFinallyReplacement {
    ~LocalFinallyReplacement() { /* Finally code goes here */ }
};
// ...
{ // some function...
    LocalFinallyReplacement lfr; // must be a named object

    // do something
}

Обратите внимание, как мы можем полностью покончить с try, catch и throw.

Если у вас были данные в функции, которая изначально была объявлена вне блока try, к которым вам требовался доступ в блоке "finally", то вам может потребоваться добавить их в конструктор вспомогательного класса и сохранить до тех пор, пока не появится деструктор.Однако на данном этапе я бы серьезно пересмотрел, можно ли решить проблему путем изменения дизайна локальных объектов обработки ресурсов, поскольку это означало бычто-то неправильное в дизайне.

Не совсем оффтопик.

Очистка ресурсов Boiler Plating DB на Java

режим сарказма:Разве идиома Java не замечательна?

За эти 15 лет я много занимался дизайном классов и оболочек шаблонов на C ++ и полностью выполнял это на языке C ++ с точки зрения очистки деструкторов.Однако каждый проект также неизменно включал использование библиотек C, которые предоставляли ресурсы с моделью использования open it, use it, close it.Попытка / finally означала бы, что такой ресурс можно просто использовать там, где это необходимо - абсолютно надежным способом - и покончить с этим.Наименее утомительный подход к программированию этой ситуации.Мог бы иметь дело со всеми другими состояниями, происходящими во время логики этой очистки, без необходимости удалять область действия в каком-либо деструкторе оболочки.

Я делал большую часть своего кодирования на C ++ в Windows, поэтому всегда мог прибегнуть к использованию Microsoft __try/__finally для таких ситуаций.(Их структурированная обработка исключений обладает некоторыми мощными возможностями для взаимодействия с исключениями.) Увы, не похоже, что язык C когда-либо ратифицировал какие-либо переносимые конструкции обработки исключений.

Однако это не было идеальным решением, потому что было непросто смешать код C и C ++ в блоке try, где могло возникнуть исключение любого стиля.Блок finally, добавленный в C ++, был бы полезен в таких ситуациях и обеспечил бы переносимость.

Что касается вашего добавления-редактировать, да, закрытие рассматривается для C ++ 0x.Их можно использовать с защитными ограждениями с областью действия RAII, чтобы обеспечить простое в использовании решение, проверьте Блог Писера.Их также можно использовать для имитации попытки-наконец, смотрите этот ответ ;но действительно ли это хорошая идея ? .

Подумал, что добавлю к этому свое собственное решение - своего рода интеллектуальную оболочку указателя, когда вам приходится иметь дело с типами, отличными от RAII.

Используется вот так:

Finaliser< IMAPITable, Releaser > contentsTable;
// now contentsTable can be used as if it were of type IMAPITable*,
// but will be automatically released when it goes out of scope.

Итак, вот реализация Finaliser:

/*  Finaliser
    Wrap an object and run some action on it when it runs out of scope.
    (A kind of 'finally.')
    * T: type of wrapped object.
    * R: type of a 'releaser' (class providing static void release( T* object )). */
template< class T, class R >
class Finaliser
{
private:
    T* object_;

public:
    explicit Finaliser( T* object = NULL )
    {
        object_ = object;
    }

    ~Finaliser() throw()
    {
        release();
    }

    Finaliser< T, R >& operator=( T* object )
    {
        if (object_ != object && object_ != NULL)
        {
            release();
        }
        object_ = object;

        return *this;
    }

    T* operator->() const
    {
        return object_;
    }

    T** operator&()
    {
        return &object_;
    }

    operator T*()
    {
        return object_;
    }

private:
    void release() throw()
    {
        R::release< T >( object_ );
    }
};

...и вот Релизер:

/*  Releaser
    Calls Release() on the object (for use with Finaliser). */
class Releaser
{
public:
    template< class T > static void release( T* object )
    {
        if (object != NULL)
        {
            object->Release();
        }
    }
};

У меня есть несколько различных видов релизеров, подобных этому, включая один для free() и один для CloseHandle().

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top