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


Ответы:


273

Нет, 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.


18
«Разрушение - это отказ от ресурсов» - DIRR ... Нет, у меня не работает. = P
Эрик Форбс

14
RAII застрял - это действительно не изменится. Попытка сделать это была бы глупой. Тем не менее, вы должны признать, что «Resource Acquisition Is Initialization» все еще довольно плохое имя.
Кевин

162
SBRM == Управление связанными ресурсами
Йоханнес Шауб -

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

54
Это застревает, когда вам нужно что-то почистить, что не соответствует времени жизни любого объекта C ++. Я полагаю, что в конечном итоге вы получите Lifetime Equals класса C ++, или, в противном случае, он становится уродливым (LECCLEOEIGU?).
Уоррен П,

79

В 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


16
// Make sure not to throw exception if one is already propagating.Для деструкторов C ++ важно не выбрасывать исключения также по этой самой причине.
Cemafor

10
@Cemafor: причина, по которой C ++ не выбрасывает исключения из деструктора, отличается от Java. В Java это будет работать (вы просто потеряете оригинальное исключение). В C ++ это действительно плохо. Но смысл в C ++ заключается в том, что вы должны сделать это только один раз (разработчиком класса), когда он пишет деструктор. В Java вы должны сделать это в момент использования. Так что ответственность за то, чтобы написать одну и ту же плиту котла, лежит на пользователе класса.
Мартин Йорк,

1
Если дело в «необходимости», вам не нужен RAII. Давайте избавимся от этого! :-) Шутки в сторону, RAII хорошо для многих случаев. RAII делает более громоздкими случаи, когда вы хотите выполнить некоторый код (не связанный с ресурсами), даже если приведенный выше код возвращается рано. Для этого вы либо используете gotos, либо разделяете его на два метода.
Тринидад

1
@Trinidad: Это не так просто, как вы думаете (так как все ваши предложения, кажется, выбирают худшие возможные варианты). Вот почему вопрос может быть лучшим местом для изучения этого, чем комментарии.
Мартин Йорк,

1
Критика «НЕ требуется из-за RAII»: есть много случаев, когда добавление специального RAII было бы слишком много стандартного кода для добавления, и try-finally просто было бы крайне уместно.
ceztko

63

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

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

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

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

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

В 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"; }

Возможная проблема: в функции 'finally (F f)' она возвращает объект FinalAction, поэтому деконструктор может быть вызван перед возвратом функции finally. Может быть, мы должны использовать std :: function вместо шаблона F.
user1633272

Обратите внимание, что FinalActionэто в основном то же самое, что и популярная ScopeGuardидиома, только с другим именем.
Андерас

1
Безопасна ли эта оптимизация?
Нулано,

2
@ Paolo.Bolzoni Извините, что не ответил раньше, я не получил уведомление о вашем комментарии. Я волновался, что блок finally (в котором я вызываю функцию DLL) будет вызываться до конца области (поскольку переменная не используется), но с тех пор нашел вопрос о SO, который снял мои опасения. Я бы связался с этим, но, к сожалению, я больше не могу его найти.
Нулано,

1
Функция disable () - своего рода бородавка в вашем, в остальном, чистом дизайне. Если вы хотите, чтобы finally вызывалось только в случае сбоя, то почему бы просто не использовать оператор catch? Разве не для этого?
user2445507

32

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

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


Это очень хороший момент. +1 тебе. Не многие другие люди проголосовали за вас, хотя. Надеюсь, вы не возражаете, что я отредактировал свой пост, чтобы включить ваши комментарии. (Я дал вам кредит конечно.) Спасибо! :)
Кевин

30

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

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

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

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

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

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


13
Точно ... RAII кажется хорошим с идеальной точки зрения. Но мне все время приходится работать с обычными C API-интерфейсами (например, функциями в C-стиле в Win32 API ...). Очень часто приобретают ресурс, который возвращает какую-то РУЧКУ, для которой затем требуется очистка некоторой функции, такой как CloseHandle (HANDLE). Использование try ... finally - хороший способ справиться с возможными исключениями. (К счастью, похоже, что shared_ptr с пользовательскими удалителями, а лямбда-символы C ++ 11 должны обеспечивать некоторую облегченность на основе RAII, которая не требует написания целых классов для переноса некоторых API, которые я использую только в одном месте.).
Джеймс Джонстон

7
@JamesJohnston, очень легко написать класс-обёртку, который содержит любой вид дескриптора и обеспечивает механику RAII. Например, ATL предоставляет их несколько. Кажется, вы считаете это слишком большой проблемой, но я не согласен, они очень маленькие и их легко написать.
Марк Рэнсом

5
Просто да, мало нет. Размер зависит от сложности библиотеки, с которой вы работаете.
Филипп Коулинг

1
@MarkRansom: Есть ли механизм, с помощью которого RAII может сделать что-то умное, если во время очистки возникает исключение, в то время как другое исключение ожидает? В системах с try / finally возможно - хотя и неудобно - расположить все так, чтобы ожидающее исключение и исключение, возникшие во время очистки, сохранялись в новом CleanupFailedException. Есть ли какой-нибудь вероятный способ достичь такого результата с помощью RAII?
суперкат

3
@couling: во многих случаях программа вызывает SomeObject.DoSomething()метод и хочет знать, успешно ли он (1), (2) не получен без побочных эффектов , (3) - с побочными эффектами, с которыми готовый справиться вызов или (4) не удалось с побочными эффектами, с которыми вызывающий абонент не может справиться. Только звонящий узнает, с какими ситуациями он может и не может справиться; то, что нужно звонящему, это способ узнать ситуацию. Жаль, что нет стандартного механизма предоставления наиболее важной информации об исключении.
Суперкат

9

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

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

    ...

1
Первый для меня наиболее читаемый из всех представленных на этой странице. +1
Никос

7

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

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

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


17
Управляемым языкам нужны окончательные блоки именно потому, что автоматически управляется только один вид ресурсов: память. RAII означает, что все ресурсы могут обрабатываться одинаково, поэтому нет необходимости в конечном итоге. Если бы вы на самом деле использовали RAII в своем примере (используя умные указатели в своем списке вместо голых), код был бы проще, чем ваш пример "finally". И даже проще, если вы не проверяете возвращаемое значение new - проверять его практически бессмысленно.
Myto

7
newне возвращает NULL, вместо этого
выдается

5
Вы поднимаете важный вопрос, но у него есть 2 возможных ответа. Одним из них является то, что дано Myto - используйте умные указатели для всех динамических распределений. Другой заключается в использовании стандартных контейнеров, которые всегда уничтожают их содержимое при уничтожении. В любом случае, каждый выделенный объект в конечном итоге принадлежит статически распределенному объекту, который автоматически освобождает его при уничтожении. Очень жаль, что эти лучшие решения трудно найти программистам из-за высокой видимости простых указателей и массивов.
j_random_hacker

4
C ++ 11 улучшает это и включает std::shared_ptrи std::unique_ptrнепосредственно в stdlib.
u0b34a0f6ae

16
Причина, по которой ваш пример выглядит так ужасно, не в том, что RAII имеет недостатки, а в том, что вы не смогли его использовать. Сырые указатели не RAII.
Бен Фойгт

6

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


4
имейте в виду, что на самом деле это не исключения C ++, а SEH. Вы можете использовать оба в коде MS C ++. SEH - это обработчик исключений ОС, который реализует исключения в VB, .NET.
gbjbaanb

и вы можете использовать SetUnhandledExceptionHandler для создания «глобального» обработчика необнаруженных исключений - для исключений SEH.
gbjbaanb

3
SEH ужасен и также не позволяет вызывать деструкторы C ++
Пол

6

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


3

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

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.


3
Я рад, что вы упомянули, что, наконец, блок может бросить; это то, что большинство ответов "use RAII", кажется, игнорируют. Чтобы избежать необходимости писать блок finally дважды, вы можете сделать что-то вродеstd::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
sethobrien

1
Это все, что я хотел знать! Почему ни один из других ответов не объяснил, что подвох (...) + пустой бросок; работает почти как наконец-то блок? Иногда вам просто нужно это.
VinGarcia

Решение, которое я предоставил в своем ответе ( stackoverflow.com/a/38701485/566849 ), должно позволять генерировать исключения изнутри finallyблока.
Фабио А.

3

Я придумал 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оператора из блоков tryand catch(), поскольку они живут в лямбда-функции;
  2. после должен быть хотя бы один catch()блок try: это требование C ++;
  3. если функция имеет возвращаемое значение, отличное от void, но в блоках tryand нет возврата catch()'s, компиляция завершится неудачно, поскольку finallyмакрос расширится до кода, который захочет вернуть a void. Это может быть, эээ, аннулируется ред при наличии finally_noreturnмакрокоманды сортов.

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


Да, это был просто быстрый взлом, но если программист знает, что он делает, это может быть полезно.
Фабио А.

@MarkLakata, я обновил пост, добавив лучшую реализацию, которая поддерживает выдачу исключений и возвратов.
Фабио А.

Выглядит хорошо. Вы можете избавиться от Caveat 2, просто вставив невозможный catch(xxx) {}блок в начале finallyмакроса, где xxx - фиктивный тип только для того, чтобы иметь хотя бы один блок catch.
Марк Лаката

@MarkLakata, я тоже об этом думал, но это сделало бы невозможным использование catch(...), не так ли?
Фабио А.

Я так не думаю. Просто создайте скрытый тип xxxв частном пространстве имен, которое никогда не будет использоваться.
Марк Лаката

2

У меня есть сценарий использования, где я считаю, что он 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);
        }
    }

Привет, я считаю, что мой ответ выше ( stackoverflow.com/a/38701485/566849 ) полностью удовлетворяет вашим требованиям.
Фабио А.

1

Как утверждают многие, решение состоит в том, чтобы использовать функции 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 ++ здесь


0

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

Если вы хотите, чтобы блок 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();
}

Это не работает, потому что весь смысл в блоке finally состоит в выполнении очистки, даже когда код должен позволить исключению покинуть блок кода. Подумайте: `try {// штука, которая, возможно, выбрасывает" B "} catch (A & a) {} finally {// если это есть в C ++ ... // штука, которая должна произойти, даже если выбрасывается" B ". } // не будет выполняться, если выброшено «B». `IMHO, исключение заключается в том, чтобы уменьшить код обработки ошибок, поэтому блоки catch, где бы ни происходил выброс, контрпродуктивны. Вот почему RAII помогает: при широком применении исключения имеют наибольшее значение в верхнем и нижнем слоях.
начале

1
@ burlyearly, хотя ваше мнение не является святым, я понимаю, но в C ++ такого нет, поэтому вы должны рассматривать это как верхний слой, который имитирует это поведение.
jave.web

СКАЧАТЬ = ПОЖАЛУЙСТА, КОММЕНТАРИЙ :)
jave.web

0

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

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

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

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


-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}

35
Симпатичная идиома, но это не совсем то же самое. возврат в блок try или catch не пройдет через ваш код finally:
Эдвард КМЕТТ

10
Стоит оставить этот неправильный ответ (с оценкой 0), так как Эдвард Кметт приводит очень важное различие.
Марк Лаката

12
Еще больший недостаток (IMO): этот код съедает все исключения, чего finallyне делает.
Бен Фойгт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.