Поддерживает ли C++ блоки finally? (А что это за "RAII", о котором я продолжаю слышать?)

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

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

В чем разница между C++ RAII идиомой и C#'использованием' заявления?

16 ответов

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

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

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

В 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

В C++11, при необходимости, RAII позволяет сделать finally:

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(); }

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

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

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

Тем не мение...

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

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

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

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

Еще одна эмуляция блока 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 :(

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

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

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

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, несмотря на то, что ресурсы все равно автоматически удаляются сборщиком мусора?

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

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 нарушают этот механизм.

Как указано в других ответах, C++ может поддерживать finallyфункциональность Реализация этой функциональности, которая, вероятно, наиболее близка к тому, чтобы быть частью стандартного языка, - это та, которая сопровождает основные руководящие принципы C++, набор лучших практик по использованию C++, отредактированный Бьярном Стоуструпом и Хербом Саттером. Реализация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 настолько близко к использованию стандартизированных функций, насколько это возможно.

У меня есть случай, когда я думаю finally должна быть вполне приемлемой частью языка C++11, так как я думаю, что ее легче читать с точки зрения потока. Мой пример использования - цепочка потоков потребитель / производитель, где часовой nullptr отправляется в конце цикла, чтобы закрыть все потоки.

Если бы C++ это поддерживал, вы бы хотели, чтобы ваш код выглядел так:

    extern Queue downstream, upstream;

    int Example()
    {
        try
        {
           while(!ExitRequested())
           {
             X* x = upstream.pop();
             if (!x) break;
             x->doSomething();
             downstream.push(x);
           } 
        }
        finally { 
            downstream.push(nullptr);
        }
    }

Я думаю, что это более логично, чем помещать ваше объявление finally в начале цикла, поскольку оно происходит после выхода из цикла... но это заблуждение, потому что мы не можем сделать это в C++. Обратите внимание, что очередь downstream подключен к другому потоку, поэтому вы не можете поставить в стражу push(nullptr) в деструкторе downstream потому что он не может быть уничтожен в этот момент... он должен оставаться в живых, пока другой поток не получит nullptr,

Итак, вот как использовать класс RAII с лямбдой, чтобы сделать то же самое:

    class Finally
    {
    public:

        Finally(std::function<void(void)> callback) : callback_(callback)
        {
        }
        ~Finally()
        {
            callback_();
        }
        std::function<void(void)> callback_;
    };

и вот как вы это используете:

    extern Queue downstream, upstream;

    int Example()
    {
        Finally atEnd([](){ 
           downstream.push(nullptr);
        });
        while(!ExitRequested())
        {
           X* x = upstream.pop();
           if (!x) break;
           x->doSomething();
           downstream.push(x);
        }
    }

Я придумал finally макрос, который можно использовать почти как 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,


AV CAVEAT: есть несколько вещей, которые не работают так же, как в Java-версии finally, С верхней части моей головы:

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

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

Не совсем, но вы можете эмулировать их в некоторой степени, например:

int * array = new int[10000000];
try {
  // Some code that can throw exceptions
  // ...
  throw std::exception();
  // ...
} catch (...) {
  // The finally-block (if an exception is thrown)
  delete[] array;
  // re-throw the exception.
  throw; 
}
// The finally-block (if no exception was thrown)
delete[] array;

Обратите внимание, что блок finally может сам выдать исключение до того, как исходное исключение будет переброшено, тем самым отбрасывая исходное исключение. Это то же самое поведение, что и в Java-блоке finally. Кроме того, вы не можете использовать return внутри блоков try&catch.

Я также думаю, что RIIA не является полностью полезной заменой для обработки исключений и наличия наконец. Кстати, я также думаю, что RIIA это плохое имя во всем мире. Я называю эти типы классов "уборщиками" и использую их МНОГО. В 95% случаев они не инициализируют и не получают ресурсы, они применяют некоторые изменения на определенной основе или принимают что-то уже настроенное и проверяют, уничтожено ли оно. Это официальное название одержимого интернетом, которое я оскорбляю, даже если предположить, что мое имя может быть лучше.

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

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

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

Я хотел бы предоставить альтернативу.

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

try{
   // something that might throw exception
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called always,
//don't forget that it might throw some exception too
doSomeCleanUp(); 

Если вы хотите, чтобы блок finally был последним, что нужно сделать при возникновении какого-либо исключения, вы можете использовать логическую локальную переменную - перед запуском установите значение false и присвойте значение true в самом конце блока try, затем после проверки блока catch для переменной значение:

bool generalAppState = false;
try{
   // something that might throw exception

   //the very end of try block:
   generalAppState = true;
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called only when exception was thrown,
//don't forget that it might throw some exception too
if( !generalAppState ){
   doSomeCleanUpOfDirtyEnd();
}

//final code to be called only when no exception is thrown
//don't forget that it might throw some exception too
else{
   cleanEnd();
}

Как утверждают многие, решение состоит в том, чтобы использовать функции C++11, чтобы избежать блоков finally. Одна из особенностей unique_ptr,

Вот ответ Мефана, написанный с использованием шаблонов RAII.

#include <vector>
#include <memory>
#include <list>
using namespace std;

class Foo
{
 ...
};

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

    for (int i = 0; i < input.size(); ++i)
    {
      myList.push_back(unique_ptr<Foo>(new Foo(input[i])));
    }

    DoSomeStuff(myList);
}

Еще одно введение в использование unique_ptr с контейнерами стандартной библиотеки C++ здесь

try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}
Другие вопросы по тегам