Чи підтримує C ++ "остаточно" блоки? (І що це за "РАЙ", про який я постійно чую?)


Відповіді:


273

Ні, C ++ не підтримує блоки "нарешті". Причина полягає в тому, що натомість 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 застряг - насправді цього не змінюється. Намагатися зробити це було б нерозумно. Однак ви повинні визнати, що "Придбання ресурсів - це ініціалізація" - це все ще досить бідне ім'я.
Кевін

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

10
Кожен, хто має навички розробляти не тільки програмне забезпечення взагалі, не кажучи вже про вдосконалені методики, не може дати жодного гідного виправдання для такої жахливої ​​абревіатури.
Хардрів

54
Це залишає вас застряглими, коли вам потрібно щось очистити, що не відповідає життю будь-якого об’єкта C ++. Я здогадуюсь, що ви закінчилися з життям, рівним C ++ класу, що перебуває на уроці або ще більше стає некрасивим (LECCLEOEIGU?).
Warren P

79

У C ++ нарешті НЕ потрібен через RAII.

RAII переносить відповідальність за безпеку винятків з користувача об'єкта на проектувальника (та реалізатора) об'єкта. Я зауважу, що це правильне місце, оскільки тоді вам потрібно лише один раз (при розробці / реалізації) отримати правильність безпеки виключень. Використовуючи нарешті, вам потрібно правити безпеку винятків під час кожного використання об'єкта.

Також код IMO виглядає акуратніше (див. Нижче).

Приклад:

Об'єкт бази даних. Щоб переконатися, що використовується з'єднання БД, його потрібно відкривати та закривати. За допомогою 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 робить використання об'єкта БД правильно дуже просто. Об'єкт БД буде правильно закриватися за допомогою деструктора незалежно від того, як ми намагатимемося і зловживати ним.

Ява, як нарешті

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.Для деструкторів С ++ важливо саме з цієї причини не кидати винятки.
Чемафор

10
@Cemafor: причина C ++ не викидати винятки з деструктора відрізняється від Java. У Java це буде працювати (ви просто втратите початковий виняток). У C ++ це дійсно погано. Але суть у C ++ полягає в тому, що робити це потрібно лише один раз (дизайнером класу), коли він пише деструктор. На Java вам потрібно зробити це на місці використання. Тому користувач класу зобов’язаний дуже довго писати одну і ту ж тарілку котла.
Мартін Йорк

1
Якщо це "необхідність", вам також не потрібен RAII. Позбудемося! :-) Жарти вбік, RAII чудово підходить для багатьох випадків. Те, що робить RAII, робить більш громіздким - це випадки, коли ви хочете виконати якийсь код (не пов'язаний з ресурсами), навіть якщо код вище повернувся достроково. Для цього або ви використовуєте gotos, або розділяєте його на два методи.
Тринідад

1
@Trinidad: Це не так просто, як ви думаєте (оскільки всі ваші пропозиції, схоже, підбирають найгірші можливі варіанти). Ось чому питання може бути кращим місцем для вивчення цього, ніж коментарі.
Мартін Йорк

1
Критикувати "НЕ потрібно через RAII": існує маса випадків, коли додавання спеціального RAII було б занадто великим кодовим кодом для додавання, а спроба, нарешті, була б вкрай доречною.
ceztko

63

RAII, як правило, краще, але ви можете легко мати остаточну семантику в C ++. Використання невеликої кількості коду.

Крім того, основні рекомендації C ++ дають нарешті.

Ось посилання на реалізацію GSL Microsoft та посилання на реалізацію Мартіна Моена

Bjarne Stroustrup кілька разів говорив, що все, що є в 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 ++.

Мати справжній клас, який керує ресурсами і таким чином уникає будь-яких витоків, як правило, краще, але це, нарешті , корисно у випадках, коли створення класу звучить як надмір.

Крім того, мені подобається це краще, ніж інші мови, нарешті, тому що, якщо ви використовуєте природний спосіб, ви пишете код закриття поблизу коду відкриття (у моєму прикладі новий і видалення ), а знищення слідує за побудовою в порядку LIFO, як зазвичай в C ++. Єдиним недоліком є ​​те, що ви отримуєте автоматичну змінну, яку ви насправді не використовуєте, і синтаксис лямбда робить її трохи галасливою (у моєму прикладі в четвертому рядку значення мають лише нарешті слово та {} -блок праворуч, відпочинок - це по суті шум).

Ще один приклад:

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

Відключити елемент є корисним , якщо , нарешті , повинен бути викликаний тільки в разі виходу з ладу. Наприклад, вам потрібно скопіювати об’єкт у три різні контейнери, ви можете налаштувати остаточно скасувати кожну копію та відключити після того, як всі копії будуть успішні. Зробивши це, якщо руйнування не вдасться кинути, ви гарантуєте сувору гарантію.

приклад відключення :

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

Можлива проблема: у функції 'нарешті (F f)' він повертає об'єкт FinalAction, тому деконструктор може бути викликаний перед поверненням функції остаточно. Можливо, нам слід використовувати функцію std :: замість шаблону F.
user1633272

Зауважте, що FinalActionв основному те саме, що і народна ScopeGuardідіома, лише з іншою назвою.
Андерас

1
Чи безпечна ця оптимізація?
Нулано

2
@ Paolo.Bolzoni Вибачте, що не відповів раніше, я не отримав сповіщення про ваш коментар. Мене хвилювало, що нарешті блок (у якому я називаю функцію DLL) буде викликаний до кінця області (тому що змінна не використовується), але з тих пір знайшов питання щодо ТА, що усунуло мої турботи. Я б посилався на це, але, на жаль, більше не можу його знайти.
Нулано

1
Функція enable () - це свого роду бородавка на вашому інакше чистому дизайні. Якщо ви хочете, щоб, нарешті, викликали лише у випадку відмови, то чому б не просто використати заяву про вилов? Хіба це не для чого?
користувач2445507

32

Крім спрощення очищення об'єктів на основі стека, RAII також корисний, оскільки те ж «автоматичне» очищення відбувається, коли об’єкт є членом іншого класу. Коли клас власності знищений, ресурс, керований класом RAII, очищається, оскільки в результаті цього викликається dtor цього класу.

Це означає, що коли ви досягаєте RAII nirvana і всі члени класу використовують RAII (як розумні покажчики), ви можете піти з дуже простим (можливо, навіть за замовчуванням) dtor класом власника, оскільки йому не потрібно вручну керувати своїм життя ресурсів учасників.


Це дуже вдалий момент. +1 вам. Хоча не багато інших людей проголосували за вас. Сподіваюся, ви не заперечуєте, що я відредагував свою публікацію, щоб включити ваші коментарі. (Я дав тобі кредит, звичайно.) Дякую! :)
Кевін

30

Чому так, що навіть керовані мови забезпечують остаточний блок, незважаючи на те, що ресурси, відповідно, автоматично перебираються сміттєзбірником?

Власне, мовам, заснованим на збирачах сміття, потрібно "нарешті" більше. Збирач сміття не знищує ваші предмети своєчасно, тому на нього не можна покластись, щоб правильно очистити проблеми, що не стосуються пам’яті.

Що стосується динамічно розподілених даних, багато хто стверджує, що вам слід використовувати смарт-покажчики.

Однак ...

RAII переносить відповідальність за безпеку винятків з користувача об'єкта на проектувальника

На жаль, це власне падіння. Старі звички програмування на C важко вмирають. Якщо ви використовуєте бібліотеку, написану на мові C, або в дуже стилі C, RAII не використовувався. Окрім того, щоб переписати весь інтерфейс API, саме з цим потрібно працювати. Тоді відсутність "нарешті" дійсно прикушує.


13
Саме ... RAII здається приємним з ідеальної точки зору. Але мені доводиться постійно працювати з звичайними API API (наприклад, функціями стилю C в API Win32 ...). Дуже часто можна придбати ресурс, який повертає якусь ручку, яка потім потребує певної функції, як CloseHandle (HANDLE) для очищення. Використання спробувати ... нарешті, це приємний спосіб боротьби з можливими винятками. (На щастя, схоже, що shared_ptr з користувацькими делетерами та C ++ 11 лямбданами повинні забезпечити полегшення на основі RAII, що не потребує написання цілих класів, щоб обернути API, який я використовую лише в одному місці.)
Джеймс Джонстон

7
@JamesJohnston, дуже просто написати клас обгортки, який містить будь-яку ручку і забезпечує механіку RAII. Наприклад, ATL пропонує купу їх. Здається, ви вважаєте це занадто великою проблемою, але я не згоден, вони дуже маленькі і їх легко написати.
Марк Рансом

5
Просте так, мале ні. Розмір залежить від складності бібліотеки, з якою ви працюєте.
Філіп Кулінг

1
@MarkRansom: Чи існує якийсь механізм, за допомогою якого RAII може зробити щось розумне, якщо виняток відбувається під час очищення, а ще виняток очікується? У системах з спробою / нарешті, можливо - хоча і незручно - впорядкувати речі так, щоб очікувані винятки та виняток, які сталися під час очищення, зберігалися в новому CleanupFailedException. Чи є якийсь правдоподібний спосіб досягти такого результату за допомогою RAII?
supercat

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

9

Ще одна "нарешті" блокова емуляція з використанням 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 ++ 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

Особисто мені не подобається "макрос" версія ідіоми "нарешті", і я вважаю за краще використовувати чисту функцію "with_finally", хоча синтаксис у цьому випадку є більш об'ємним.

Ви можете перевірити код вище: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

PS

Якщо вам потрібен остаточний блок у вашому коді, то масштабні охоронці або макроси 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 переносить відповідальність за безпеку винятків з користувача об'єкта на проектувальника (та реалізатора) об'єкта. Я зауважу, що це правильне місце, оскільки тоді вам потрібно лише один раз (при розробці / реалізації) отримати правильність безпеки виключень. Використовуючи нарешті, вам потрібно правити безпеку винятків під час кожного використання об'єкта.

Частіше за все вам доводиться мати справу з динамічно виділеними об'єктами, динамічними номерами об'єктів і т. Д. У межах блоку спробу деякий код може створити багато об'єктів (скільки визначається під час виконання) та зберігати покажчики на них у списку. Зараз це не екзотичний сценарій, але дуже поширений. У цьому випадку ви хочете писати такі речі

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

Крім того: чому так, що навіть керовані місця проживання забезпечують остаточний блок, незважаючи на те, що ресурси сміттєзбірника автоматично розподіляються автоматично?

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


17
Керовані мови потрібно, нарешті, блокувати саме тому, що автоматично керується лише одним видом ресурсу: пам'яттю. RAII означає, що з усіма ресурсами можна обробляти однаково, тому остаточно не потрібно. Якщо ви фактично використовували RAII у своєму прикладі (використовуючи розумні вказівники у своєму списку замість оголених), код буде простішим, ніж ваш "нарешті" -приклад. А ще простіше, якщо ви не перевіряєте повернене значення нового - перевіряти його майже безглуздо.
Myto

7
newне повертає NULL, він замість цього викидає виняток
Hasturkun

5
Ви ставите важливе питання, але на нього є 2 можливих відповіді. Одне - це те, що дає Myto - використовувати розумні покажчики для всіх динамічних розподілів. Інша полягає у використанні стандартних контейнерів, які завжди руйнують їх вміст при знищенні. Так чи інакше, кожен виділений об'єкт належить в кінцевому підсумку статично виділеному об'єкту, який автоматично звільняє його після знищення. Дуже шкода, що ці кращі рішення важко знайти програмістам через високу видимість простих покажчиків та масивів.
j_random_hacker

4
C ++ 11 покращує це, включаючи std::shared_ptrта std::unique_ptrбезпосередньо в stdlib.
u0b34a0f6ae

16
Причина, по якій ваш приклад виглядає так жахливо, полягає не в тому, що RAII є хибним, а це тому, що ви не змогли його використати. Сирі покажчики не є RAII.
Ben Voigt

6

FWIW, Microsoft Visual C ++ все ж підтримує спробу, нарешті, вона історично використовувалася в програмах 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 ++ за редакцією Bjarne Stoustrup та Herb Sutter. Реалізаціяfinally є частиною бібліотеки Guidelines підтримки (GSL). У Посібниках використання finallyрекомендується використовувати при роботі з інтерфейсами старого стилю, а також має власне керівництво під назвою Використовувати кінцевий об'єкт запуску для вираження очищення, якщо не існує відповідної обробки ресурсів .

Отже, не тільки підтримується 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;

Зауважте, що остаточний блок може сам викинути виняток перед повторним скиданням вихідного винятку, тим самим відкидаючи вихідний виняток. Це точно така ж поведінка, як і в остаточному блоці Java. Крім того, ви не можете використовувати returnвсередині блоків try & catch.


3
Я радий, що ви згадали, що нарешті блок може кинутись; справа в тому, що більшість відповідей на "використання RAII", здається, ігноруються. Щоб уникнути необхідності написати остаточний блок двічі, ви можете зробити щось на кшталт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 розширення, яке також підтримується кланг.

УВАГА : більш рання версія цієї відповіді використовувала іншу реалізацію концепції з багатьма обмеженнями більше.

Спочатку давайте визначимо клас помічника.

#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.


¹ CAVEAT: є кілька речей, які працюють не так, як версія java finally. Вгорі голови:

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

Загалом, я не знаю, чи коли-небудь я сам використовував би цей матеріал, але грати з ним було весело. :)


Так, це був просто швидкий злом, але якщо програміст знає, що вони роблять, це все-таки може бути корисним.
Фабіо А.

@MarkLakata, я оновив публікацію з кращою реалізацією, яка підтримує викидання винятків та повернення.
Фабіо А.

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

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

Я думаю, що це більш логічно, якщо ставити остаточну заяву на початок циклу, оскільки це відбувається після виходу циклу ... але це бажано думати, оскільки ми не можемо це зробити в 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, щоб уникнути блокування остаточно. Однією з особливостей є 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);
}

Ще кілька знайомств із використанням унікального_ptr з контейнерами стандартної бібліотеки C ++ є тут


0

Я хотів би запропонувати альтернативу.

Якщо ви хочете, щоб нарешті блок називався завжди, просто поставте його після останнього блоку лову (який, мабуть, має бути 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(); 

Якщо ви хочете, нарешті, блокувати як останнє, що потрібно зробити, коли викинуто будь-який виняток, ви можете використовувати булеву локальну змінну - перед запуском ви встановите її на помилку та поставите справжнє призначення в самому кінці блоку спробу, а потім після блоку захоплення перевіряйте змінну значення:

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

Це не працює, оскільки вся суть остаточного блоку полягає у виконанні очищення, навіть коли код повинен дозволяти виняток залишити блок коду. Поміркуйте: `спробуйте {// речі, можливо, кидаючи" B "} ловити (A & a) {} нарешті {// якщо в C ++ це було ... // речі, які повинні відбутися, навіть якщо" B "кинуто. } // не виконає, якщо "B" кинуто. `IMHO, суть винятків полягає у зменшенні коду керування помилками, тому блоки вилову, де б не відбувся викид, є контрпродуктивним. Ось чому РАІІ допомагає: якщо застосовуватись вільно, винятки мають найбільше значення у верхньому та нижньому шарах.
burlyearly

1
@burlyearly, хоча ваша думка не свята, я розумію, але в C ++ такого немає, тому ви повинні розглядати це як верхній шар, що імітує цю поведінку.
jave.web

СКАЧАЙТЕ = КОМЕНДУЙТЕ КОМАНДА :)
jave.web

0

Я також вважаю, що RIIA не є повністю корисною заміною для обробки винятків і, нарешті,. До речі, я також думаю, що RIIA - це погана назва навколо. Я називаю ці типи класів «двірниками» і використовую їх ЛОТ. У 95% часу вони не ініціалізують і не набувають ресурсів, вони застосовують певні зміни на основі масштабів, або беруть щось вже створене і переконуються, що воно знищене. Це офіційне найменування одержимого Інтернету, я зловживаюсь тим, що навіть припускаю, що моє ім’я може бути кращим.

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

Так, це добре для класів, призначених для управління певним ресурсом, або загальних, які призначені для обробки набору подібних ресурсів. Але навіть якщо у всіх речах є такі обгортки, координація очищення може бути не просто простим викликом деструкторів у зворотному порядку.

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


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

35
Мила ідіома, але це не зовсім те саме. повернення в спробу блоку або лову не пройде через ваш "нарешті:" код.
Едвард КМЕТТ

10
Варто зберегти цю неправильну відповідь (з 0 оцінкою), оскільки Едуард Кметт виявляє дуже важливу відмінність.
Марк Лаката

12
Ще більший недолік (IMO): Цей код їсть всі винятки, що finallyне робить.
Бен Войгт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.