Есть ли предпочтительная идиома для имитации попытки / окончательного использования Java в C ++? - PullRequest
13 голосов
/ 01 февраля 2009

Я занимался Java уже много лет, поэтому не отслеживал C ++. Было ли добавлено предложение finally к обработке исключений в C ++ в определении языка?

Есть ли любимая идиома, которая имитирует попытку / окончание Java?

Я также обеспокоен тем, что в 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 ++?

Ответы [ 15 ]

26 голосов
/ 01 февраля 2009

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

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

ОБНОВЛЕНИЕ : прямо изо рта лошади: Почему C ++ не предоставляет конструкцию "finally"?

15 голосов
/ 01 февраля 2009

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

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

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

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

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

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

11 голосов
/ 01 февраля 2009

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

Чтение http://en.wikipedia.org/wiki/Resource_acquisition_is_initialization

10 голосов
/ 01 февраля 2009

Нет, наконец, не был добавлен в C ++, и вряд ли он когда-либо будет добавлен.

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

Хотя использовать его не обязательно, в C ++ есть исключение std ::.
Принуждение разработчиков к наследованию от конкретного класса использовать исключение противоречит простой философии C ++. Также мы не требуем, чтобы все классы были производными от Object.

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

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

9 голосов
/ 01 февраля 2009

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5 голосов
/ 02 февраля 2009

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

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

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

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

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

Java была бы намного лучше, если бы вместо RAI реализовывался RAII ... finally.

4 голосов
/ 02 февраля 2009

Чтобы избежать необходимости определять класс-оболочку для каждого высвобождаемого ресурса, вас может заинтересовать 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);
3 голосов
/ 02 февраля 2009

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

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

В С ++

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 ++, вы перекладываете ответственность за правильное использование на разработчика / разработчика класса. Это наследственно безопаснее, так как проектировщику нужно делать это правильно только один раз на уровне класса (вместо того, чтобы разные пользователи пытались делать это правильно разными способами).

2 голосов
/ 05 марта 2013

Используя 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()> будет использовать некоторую косвенность указателя и, по крайней мере, один вызов виртуальной функции для выполнения стирания типа , поэтому будет снижение производительности, Не используйте эту технику в узком цикле, где производительность критична. В этих случаях более подходящим был бы специализированный объект, деструктор которого делает только одну вещь.

1 голос
/ 02 февраля 2009

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

Очистка ресурсов БД в Java. 1004 *

режим сарказма: разве идиома Java не прекрасна?

...