Поддерживает ли C ++ блоки finally? (А что это за «RAII», о котором я продолжаю слышать?) - PullRequest
249 голосов
/ 02 октября 2008

Поддерживает ли C ++ блоки ' finally '?

Что такое RAII идиома ?

В чем разница между C ++ идиомой RAII и C # 'using' оператором ?

Ответы [ 16 ]

252 голосов
/ 02 октября 2008

Нет, C ++ не поддерживает блоки finally. Причина в том, что C ++ вместо этого поддерживает RAII: «Приобретение ресурсов - это инициализация» - плохое имя для действительно полезной концепции.

Идея состоит в том, что деструктор объекта отвечает за освобождение ресурсов. Когда объект имеет автоматическую продолжительность хранения, деструктор объекта будет вызываться при выходе из блока, в котором он был создан, даже когда этот блок выходит при наличии исключения. Вот объяснение Бьярна Страуструпа темы.

Обычное использование RAII - блокировка мьютекса:

// A class with implements RAII
class lock
{
    mutex &m_;

public:
    lock(mutex &m)
      : m_(m)
    {
        m.acquire();
    }
    ~lock()
    {
        m_.release();
    }
};

// A class which uses 'mutex' and 'lock' objects
class foo
{
    mutex mutex_; // mutex for locking 'foo' object
public:
    void bar()
    {
        lock scopeLock(mutex_); // lock object.

        foobar(); // an operation which may throw an exception

        // scopeLock will be destructed even if an exception
        // occurs, which will release the mutex and allow
        // other functions to lock the object and run.
    }
};

RAII также упрощает использование объектов в качестве членов других классов. При уничтожении класса-владельца ресурс, управляемый классом RAII, освобождается, потому что в результате вызывается деструктор для класса, управляемого RAII. Это означает, что когда вы используете RAII для всех членов класса, которые управляют ресурсами, вы можете использовать очень простой, может быть, даже стандартный деструктор для класса владельца, поскольку ему не нужно вручную управлять временем жизни его члена-члена , (Спасибо Mike B за указание на это.)

Для тех, кто знаком с C # или VB.NET, вы можете распознать, что RAII похож на .NET детерминированное уничтожение с использованием операторов IDisposable и «using» . Действительно, два метода очень похожи. Основное отличие состоит в том, что RAII детерминистически освобождает любой тип ресурса, включая память. При реализации IDisposable в .NET (даже на языке .NET C ++ / CLI) ресурсы будут освобождаться детерминированным образом, кроме памяти. В .NET память не освобождается детерминистически; память освобождается только во время циклов сборки мусора.

† Некоторые люди считают, что «Разрушение - это отказ от ресурсов» - более точное название идиомы RAII.

74 голосов
/ 02 октября 2008

В C ++, наконец, НЕ требуется из-за RAII.

RAII переносит ответственность за безопасность исключений от пользователя объекта к разработчику (и исполнителю) объекта. Я бы сказал, что это правильное место, так как вам нужно только один раз получить правильную безопасность исключений (при разработке / реализации). Используя, наконец, вам нужно корректировать безопасность исключений каждый раз, когда вы используете объект.

Также ИМО код выглядит аккуратнее (см. Ниже).

Пример:

Объект базы данных. Чтобы убедиться, что соединение с БД используется, оно должно быть открыто и закрыто. С помощью RAII это можно сделать в конструкторе / деструкторе.

C ++ Как RAII

void someFunc()
{
    DB    db("DBDesciptionString");
    // Use the db object.

} // db goes out of scope and destructor closes the connection.
  // This happens even in the presence of exceptions.

Использование RAII значительно упрощает использование объекта БД. Объект БД будет корректно закрываться при использовании деструктора, независимо от того, как мы пытаемся его использовать.

Java, как наконец

void someFunc()
{
    DB      db = new DB("DBDesciptionString");
    try
    {
        // Use the db object.
    }
    finally
    {
        // Can not rely on finaliser.
        // So we must explicitly close the connection.
        try
        {
            db.close();
        }
        catch(Throwable e)
        {
           /* Ignore */
           // Make sure not to throw exception if one is already propagating.
        }
    }
}

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

Также это простой пример.
Когда у вас есть несколько ресурсов, которые необходимо освободить, код может усложниться.

Более подробный анализ можно найти здесь: http://accu.org/index.php/journals/236

54 голосов
/ 26 августа 2014

RAII обычно лучше, но вы можете легко иметь семантику finally в C ++. Используя небольшое количество кода.

Кроме того, основные рекомендации C ++ дают окончательно.

Вот ссылка на реализацию GSL Microsoft и ссылка на реализацию Martin Moene

Бьярн Страуструп несколько раз говорил, что все, что есть в GSL, в конечном итоге должно войти в стандарт. Так что это должен быть ориентированный на будущее способ использования finally .

Вы можете легко реализовать себя, если хотите, продолжайте читать.

В C ++ 11 RAII и лямбда-выражения позволяют окончательно сделать генерал:

namespace detail { //adapt to your "private" namespace
template <typename F>
struct FinalAction {
    FinalAction(F f) : clean_{f} {}
   ~FinalAction() { if(enabled_) clean_(); }
    void disable() { enabled_ = false; };
  private:
    F clean_;
    bool enabled_{true}; }; }

template <typename F>
detail::FinalAction<F> finally(F f) {
    return detail::FinalAction<F>(f); }

пример использования:

#include <iostream>
int main() {
    int* a = new int;
    auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!\n"; });
    std::cout << "doing something ...\n"; }

вывод будет:

doing something...
leaving the block, deleting a!

Лично я использовал это несколько раз, чтобы обеспечить закрытие дескриптора файла POSIX в программе на C ++.

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

Кроме того, мне нравится это лучше, чем другие языки наконец , потому что при естественном использовании вы пишете закрывающий код рядом с кодом открытия (в моем примере new и delete ), а разрушение следует за конструкцией в порядке LIFO, как обычно в C ++. Единственным недостатком является то, что вы получаете автоматическую переменную, которую вы на самом деле не используете, а лямбда-синтаксис делает ее немного шумной (в моем примере в четвертой строке только слово finally и блок {} на права имеют смысл, остальное по сути шум).

Другой пример:

 [...]
 auto precision = std::cout.precision();
 auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } );
 std::cout << std::setprecision(3);

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

отключить пример:

//strong guarantee
void copy_to_all(BIGobj const& a) {
    first_.push_back(a);
    auto undo_first_push = finally([first_&] { first_.pop_back(); });

    second_.push_back(a);
    auto undo_second_push = finally([second_&] { second_.pop_back(); });

    third_.push_back(a);
    //no necessary, put just to make easier to add containers in the future
    auto undo_third_push = finally([third_&] { third_.pop_back(); });

    undo_first_push.disable();
    undo_second_push.disable();
    undo_third_push.disable(); }

Если вы не можете использовать C ++ 11, у вас все равно может быть наконец , но код становится немного длиннее. Просто определите структуру только с помощью конструктора и деструктора, конструктор берет ссылки на все, что нужно, а деструктор выполняет необходимые вам действия. Это в основном то, что делает лямбда, сделанная вручную.

#include <iostream>
int main() {
    int* a = new int;

    struct Delete_a_t {
        Delete_a_t(int* p) : p_(p) {}
       ~Delete_a_t() { delete p_; std::cout << "leaving the block, deleting a!\n"; }
        int* p_;
    } delete_a(a);

    std::cout << "doing something ...\n"; }
30 голосов
/ 21 сентября 2010

Почему даже управляемые языки обеспечивают блок finally, несмотря на то, что ресурсы все равно автоматически удаляются сборщиком мусора?

На самом деле, языки, основанные на сборщиках мусора, нуждаются в «наконец-то» больше. Сборщик мусора не уничтожает ваши объекты своевременно, поэтому на него нельзя полагаться для правильного устранения проблем, не связанных с памятью.

Что касается динамически распределяемых данных, многие утверждают, что вам следует использовать умные указатели.

Однако ...

RAII переносит ответственность за безопасность исключений от пользователя объекта к конструктору

К сожалению, это его собственное падение. Старые привычки программирования на С сильно умирают. Когда вы используете библиотеку, написанную на C или в стиле C, RAII не будет использоваться. Если не считать переписывания всего API-интерфейса, то именно с этим вам и придется работать. Тогда отсутствие «наконец» действительно кусается.

30 голосов
/ 02 октября 2008

Помимо упрощения очистки с помощью основанных на стеке объектов, RAII также полезен, поскольку такая же «автоматическая» очистка происходит, когда объект является членом другого класса. Когда класс-владелец уничтожается, ресурс, управляемый классом RAII, очищается, потому что в результате вызывается dtor для этого класса.

Это означает, что когда вы достигаете нирваны RAII, и все члены класса используют RAII (например, умные указатели), вы можете получить очень простой (возможно, даже стандартный) dtor для класса владельца, так как ему не нужно вручную управлять временем жизни его члена.

7 голосов
/ 30 ноября 2017

Еще одна эмуляция блока finally с использованием лямбда-функций C ++ 11

template <typename TCode, typename TFinallyCode>
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
    try
    {
        code();
    }
    catch (...)
    {
        try
        {
            finally_code();
        }
        catch (...) // Maybe stupid check that finally_code mustn't throw.
        {
            std::terminate();
        }
        throw;
    }
    finally_code();
}

Будем надеяться, что компилятор оптимизирует приведенный выше код.

Теперь мы можем написать код, подобный этому:

with_finally(
    [&]()
    {
        try
        {
            // Doing some stuff that may throw an exception
        }
        catch (const exception1 &)
        {
            // Handling first class of exceptions
        }
        catch (const exception2 &)
        {
            // Handling another class of exceptions
        }
        // Some classes of exceptions can be still unhandled
    },
    [&]() // finally
    {
        // This code will be executed in all three cases:
        //   1) exception was not thrown at all
        //   2) exception was handled by one of the "catch" blocks above
        //   3) exception was not handled by any of the "catch" block above
    }
);

Если вы хотите, чтобы вы могли обернуть эту идиому в макрос «попробуй - наконец»:

// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};

#define begin_try    with_finally([&](){ try
#define finally      catch(never_thrown_exception){throw;} },[&]()
#define end_try      ) // sorry for "pascalish" style :(

Теперь в C ++ доступен блок "finally":

begin_try
{
    // A code that may throw
}
catch (const some_exception &)
{
    // Handling some exceptions
}
finally
{
    // A code that is always executed
}
end_try; // Sorry again for this ugly thing

Лично мне не нравится идиома "макроса" версии "finally", и я предпочел бы использовать чистую функцию "with_finally", даже если в этом случае синтаксис более громоздкий.

Вы можете проверить код выше здесь: http://coliru.stacked -crooked.com / a / 1d88f64cb27b3813

PS

Если вам нужен блок finally в вашем коде, то макросы охранники или ON_FINALLY / ON_EXCEPTION , вероятно, лучше подойдут вам.

Вот краткий пример использования ON_FINALLY / ON_EXCEPTION:

void function(std::vector<const char*> &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    ...
7 голосов
/ 02 июня 2010

Извините, что выкопал такую ​​старую ветку, но в следующих рассуждениях есть серьезная ошибка:

RAII переносит ответственность за безопасность исключений от пользователя объекта к разработчику (и исполнителю) объекта. Я бы сказал, что это правильное место, так как вам нужно только один раз получить правильную безопасность исключений (при разработке / реализации). Используя, наконец, вам нужно корректировать безопасность исключений каждый раз, когда вы используете объект.

Чаще всего вам приходится иметь дело с динамически размещаемыми объектами, динамическим числом объектов и т. Д. В блоке try некоторый код может создавать множество объектов (сколько определяется во время выполнения) и сохранять указатели на них в список. Теперь это не экзотический сценарий, но очень распространенный. В этом случае вы захотите написать что-то вроде

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  finally
  {
    while (!myList.empty())
    {
      delete myList.back();
      myList.pop_back();
    }
  }
}

Конечно, сам список будет уничтожен при выходе из области видимости, но это не приведет к очистке созданных вами временных объектов.

Вместо этого вы должны идти по уродливому маршруту:

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  catch(...)
  {
  }

  while (!myList.empty())
  {
    delete myList.back();
    myList.pop_back();
  }
}

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

Подсказка: с «окончанием» вы можете сделать больше, чем просто освободить память.

5 голосов
/ 08 августа 2018

Как указано в других ответах, C ++ может поддерживать finally -подобную функциональность. Реализация этой функциональности, которая, вероятно, наиболее близка к тому, чтобы быть частью стандартного языка, - это та, которая сопровождает C ++ Core Guidelines , набор лучших практик по использованию C ++, отредактированный Bjarne Stoustrup и Herb Sutter. реализация finally является частью библиотеки поддержки рекомендаций (GSL). Повсюду в Руководстве рекомендуется использовать finally при работе с интерфейсами старого стиля, и у него также есть собственное руководство под названием Использовать объект final_action для экспресс-очистки, если нет подходящего дескриптора ресурса .

Таким образом, C ++ не только поддерживает finally, но и рекомендуется использовать его во многих типичных случаях.

Пример использования реализации GSL будет выглядеть так:

#include <gsl/gsl_util.h>

void example()
{
    int handle = get_some_resource();
    auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });

    // Do a lot of stuff, return early and throw exceptions.
    // clean_that_resource will always get called.
}

Внедрение и использование GSL очень похоже на то, что было в Ответ Паоло Больцони . Единственное отличие состоит в том, что объекту, созданному gsl::finally(), не хватает вызова disable(). Если вам нужна эта функциональность (скажем, для возврата ресурса после того, как он собран, и исключений не должно быть), вы можете предпочесть реализацию Paolo. В противном случае использование GSL настолько же близко к использованию стандартизированных функций, сколько вы получите.

5 голосов
/ 02 октября 2008

FWIW, Microsoft Visual C ++ поддерживает try, наконец, и он исторически использовался в приложениях MFC как метод отлова серьезных исключений, которые в противном случае могли бы привести к сбою. Например;

int CMyApp::Run() 
{
    __try
    {
        int i = CWinApp::Run();
        m_Exitok = MAGIC_EXIT_NO;
        return i;
    }
    __finally
    {
        if (m_Exitok != MAGIC_EXIT_NO)
            FaultHandler();
    }
}

Я использовал это в прошлом, чтобы делать такие вещи, как сохранение резервных копий открытых файлов перед выходом. Некоторые параметры отладки JIT нарушают этот механизм.

3 голосов
/ 01 августа 2016

Я придумал макрос finally, который можно использовать почти как keyword ключевое слово finally в Java; он использует std::exception_ptr и друзей, лямбда-функции и std::promise, поэтому он требует C++11 или выше; он также использует составное выражение выражение расширение GCC, которое также поддерживается clang.

ПРЕДУПРЕЖДЕНИЕ : более ранняя версия этого ответа использовала другую реализацию концепции со многими другими ограничениями.

Сначала давайте определим вспомогательный класс.

#include <future>

template <typename Fun>
class FinallyHelper {
    template <typename T> struct TypeWrapper {};
    using Return = typename std::result_of<Fun()>::type;

public:    
    FinallyHelper(Fun body) {
        try {
            execute(TypeWrapper<Return>(), body);
        }
        catch(...) {
            m_promise.set_exception(std::current_exception());
        }
    }

    Return get() {
        return m_promise.get_future().get();
    }

private:
    template <typename T>
    void execute(T, Fun body) {
        m_promise.set_value(body());
    }

    void execute(TypeWrapper<void>, Fun body) {
        body();
    }

    std::promise<Return> m_promise;
};

template <typename Fun>
FinallyHelper<Fun> make_finally_helper(Fun body) {
    return FinallyHelper<Fun>(body);
}

Тогда есть фактический макрос.

#define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try 
#define finally });                         \
        true;                               \
        ({return __finally_helper.get();})) \
/***/

Может использоваться следующим образом:

void test() {
    try_with_finally {
        raise_exception();
    }    

    catch(const my_exception1&) {
        /*...*/
    }

    catch(const my_exception2&) {
        /*...*/
    }

    finally {
        clean_it_all_up();
    }    
}

Использование std::promise делает его очень простым в реализации, но, вероятно, оно также вносит немало ненужных накладных расходов, которых можно избежать, реализуя только необходимые функции из std::promise.


¹ ПРЕДУПРЕЖДЕНИЕ: есть несколько вещей, которые не работают так же, как java-версия finally. С макушки головы:

  1. невозможно вырваться из внешнего цикла с помощью оператора break из блоков try и catch(), поскольку они живут в лямбда-функции;
  2. должен быть хотя бы один catch() блок после try: это требование C ++;
  3. если функция имеет возвращаемое значение, отличное от void, но в блоках try и catch()'s возврата нет, компиляция завершится неудачно, поскольку макрос finally развернется до кода, который захочет вернуть void , Это может быть ошибкой void ed при наличии своего рода макроса finally_noreturn.

В общем, я не знаю, буду ли я когда-либо использовать этот материал сам, но было весело играть с ним. :)

...