Существуют ли случаи, когда конструкция "finally" будет полезна в C ++? - PullRequest
6 голосов
/ 22 декабря 2008

Бьярн Страуструп пишет в своем FAQ по стилю и технике C ++ , акцент мой:

Поскольку C ++ поддерживает альтернативу, которая почти всегда лучше : метод "получение ресурсов является инициализацией" (TC ++ PL3, раздел 14.4). Основная идея заключается в представлении ресурса локальным объектом, чтобы деструктор локального объекта освободил ресурс. Таким образом, программист не может забыть освободить ресурс. Например:

class File_handle {
    FILE* p;
public:
    File_handle(const char* n, const char* a)
        { p = fopen(n,a); if (p==0) throw Open_error(errno); }
    File_handle(FILE* pp)
        { p = pp; if (p==0) throw Open_error(errno); }

    ~File_handle() { fclose(p); }

    operator FILE*() { return p; }

    // ...
};

void f(const char* fn)
{
    File_handle f(fn,"rw"); // open fn for reading and writing
    // use file through f
}

В системе нам нужен класс «описатель ресурса» для каждого ресурса. Тем не менее, мы не должны иметь предложение "finally" для каждого приобретения ресурса. В реалистичных системах число операций получения ресурсов намного больше, чем видов ресурсов, поэтому метод «получение ресурсов является инициализацией» приводит к меньшему количеству кода, чем использование конструкции «окончательный вариант».

Обратите внимание, что Бьярне пишет «почти всегда лучше», а не «всегда лучше». Теперь мой вопрос: в какой ситуации конструкция finally будет лучше, чем использование альтернативной конструкции (RAII) в C ++?

Ответы [ 6 ]

7 голосов
/ 22 декабря 2008

Разница между ними состоит в том, что деструкторы подчеркивают повторное использование решения очистки, связывая его с используемым типом, тогда как try / finally подчеркивает одноразовые процедуры очистки. Так что try / finally более удобен, когда у вас есть уникальное одноразовое требование очистки, связанное с точкой использования, а не решение для повторного использования, которое может быть связано с типом, который вы используете.

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

try_finally([]
{
    // attempt to do things in here, perhaps throwing...
},
[]
{
    // this always runs, even if the above block throws...
}

Конечно, вы должны написать try_ finally, но только один раз, и тогда все готово. Лямбды позволяют создавать новые структуры управления.

Что-то вроде:

template <class TTry, class TFinally>
void try_finally(const TTry &tr, const TFinally &fi)
{
    try
    {
        tr();
    }
    catch (...)
    {
        fi();
        throw;
    }

    fi();
}

И вообще нет никакой связи между наличием GC и предпочтением try / finally вместо деструкторов. C ++ / CLI имеет деструкторы и GC. Это ортогональный выбор. Try / finally, и деструкторы - это несколько разные решения одной и той же проблемы, оба детерминированные, необходимые для нереальных ресурсов.

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

6 голосов
/ 22 декабря 2008

Единственная причина, по которой я могу думать о том, что блок finally будет «лучше», - это когда для выполнения той же задачи требуется меньше кода. Например, если у вас есть ресурс, который по какой-то причине не использует RIIA, вам нужно либо написать класс, чтобы обернуть ресурс и освободить его в деструкторе, либо использовать блок finally (если он существовал).

Сравнить:

class RAII_Wrapper
{
    Resource *resource;

public:
    RAII_Wrapper() : resource(aquire_resource()) {}

    ~RAII_Wrapper() {
        free_resource(resource);
        delete resource;
    }

    Resource *getResource() const {
        return resource;
    }
};

void Process()
{
    RAII_Resource wrapper;
    do_something(wrapper.resource);
}

против

void Process()
{
    try {
        Resource *resource = aquire_resource();
        do_something(resource);
    }
    finally {
        free_resource(resource);
        delete resource;
    }
}

Большинство людей (включая меня) по-прежнему утверждают, что первая версия лучше, потому что она не заставляет вас использовать блок try ... finally. Вам также нужно написать класс только один раз, а не дублировать код в каждой функции, которая использует ресурс.

Редактировать: Как упомянуто litb, вы должны использовать auto_ptr вместо удаления указателей вручную, что упростит оба случая.

6 голосов
/ 22 декабря 2008

Наконец-то будет лучше при соединении с C-кодом. Это может быть боль, когда приходится оборачивать существующие функции C в RAII.

3 голосов
/ 22 декабря 2008

Я думаю, что scope guard хорошо справляется с одноразовыми случаями, которые в конечном итоге хорошо обрабатываются, и в то же время лучше в более общем смысле, поскольку хорошо обрабатывает несколько потоков. *

1 голос
/ 05 января 2018

Основное использование, которое я нашел бы для finally, было бы при работе с кодом C, поскольку другие указали, что ресурс C может использоваться только один или два раза в коде и не стоит включать в структуру, соответствующую RAII. Тем не менее, с лямбдами, кажется, достаточно просто вызвать некоторую пользовательскую логику через dtor, вызывающий объект функции, который мы указываем в самой функции.

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

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

Это упростило бы средства защиты области действия, чтобы больше не требовать вызова commit/dismiss для принятия изменений без автоматического отката их при уничтожении защиты области действия. Идея состоит в том, чтобы позволить это:

ScopeGuard guard(...);

// Cause external side effects.
...

// If we managed to reach this point without facing an exception,
// dismiss/commit the changes so that the guard won't undo them
// on destruction.
guard.dismiss();

Чтобы просто стать этим:

ScopeGuard guard(...);

// Cause external side effects.
...

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

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

0 голосов
/ 22 декабря 2008

Редактировать после шести ответов.

А как насчет этого:

class Exception : public Exception { public: virtual bool isException() { return true; } };
class NoException : public Exception { public: bool isException() { return false; } };


Object *myObject = 0;

try
{
  try
  {
    myObject = new Object(); // Create an object (Might throw exception)
  }
  catch (Exception &e)
  {
    // Do something with exception (Might throw if unhandled)
  }

  throw NoException();
}
catch (Exception &e)
{
  delete myObject;

  if (e.isException()) throw e;
}
...