Когда RAII имеет преимущество перед GC? - PullRequest
13 голосов
/ 03 января 2012

Рассмотрим этот простой класс, который демонстрирует RAII в C ++ (из головы):

class X {
public:
    X() {
      fp = fopen("whatever", "r");
      if (fp == NULL) 
        throw some_exception();
    }

    ~X() {
        if (fclose(fp) != 0){
            // An error.  Now what?
        }
    }
private:
    FILE *fp;
    X(X const&) = delete;
    X(X&&) = delete;
    X& operator=(X const&) = delete;
    X& operator=(X&&) = delete;
}

Я не могу выбросить исключение в деструкторе.У меня ошибка, но я не могу сообщить об этом.И этот пример довольно общий: я могу сделать это не только с файлами, но и, например, с помощью потоков posix, графических ресурсов, ... Я отмечаю, как, например, страница RAII в Википедии охватывает весь вопрос под заголовком: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

Мне кажется, что RAII будет полезен, только если уничтожение гарантированно произойдет без ошибок.Единственные известные мне ресурсы с этим свойством - это память.Теперь мне кажется, что, например, Бем довольно убедительно опровергает идею ручного управления памятью - это хорошая идея в любой распространенной ситуации, так где же когда-либо преимущество в использовании CII в RAII?

Да, я знаю, что GC немного еретичен в мире C ++; -)

Ответы [ 8 ]

14 голосов
/ 03 января 2012

RAII, в отличие от GC, является детерминированным .Вы будете точно знать, когда ресурс будет выпущен, в отличие от того, «когда-нибудь в будущем он будет выпущен», в зависимости от того, когда GC решит, что ему нужно снова запустить.

Теперь перейдем к актуальной проблеме.Вы, кажется, имеете.Эта дискуссия возникла в LoungeНекоторое время назад в чате говорили о том, что вам следует делать, если деструктор объекта RAII может потерпеть неудачу.

Был сделан вывод, что лучшим способом будет предоставить конкретную close(), destroy() или аналогичную функцию-членон вызывается деструктором, но также может вызываться и до этого, если вы хотите обойти проблему «исключения во время разматывания стека».Затем он установит флаг, который остановит его от вызова в деструкторе.Например, std::(i|o)fstream делает именно это - он закрывает файл в своем деструкторе, но также предоставляет метод close().

13 голосов
/ 03 января 2012

Это аргумент соломенного человека, потому что вы не говорите о сборке мусора (освобождении памяти), вы говорите об общем управлении ресурсами.

Если вы неправильно использовали сборщик мусора для закрытия файлов таким образом, у вас возникла бы идентичная ситуация: вы также не могли бы генерировать исключение. Вам будут доступны те же параметры: игнорирование ошибки или, что гораздо лучше, ее регистрация.

7 голосов
/ 03 января 2012

Точно такая же проблема возникает при сборке мусора.

Тем не менее, стоит отметить, что если в вашем коде или в коде библиотеки, который поддерживает ваш код, нет ошибок, удаление ресурса никогда завершится неудачно. delete никогда не подведет, если вы не испортили свою кучу. Это та же история для каждого ресурса. Неспособность уничтожить ресурс - это аварийное завершение работы приложения, а не приятное исключение «обработай меня».

4 голосов
/ 03 января 2012

Я хочу добавить еще несколько мыслей, касающихся "RAII" против GC.Аспекты использования какой-либо функции «закрыть», «уничтожить», «завершить», любую функцию уже объяснены, так как это аспект освобождения детерминированного ресурса.Есть, по крайней мере, еще два важных средства, которые включаются с помощью деструкторов и, таким образом, отслеживания ресурсов управляемым программистом способом:

  1. В мире RAII можно иметь устаревшиеуказатель, т.е. указатель, который указывает на уже уничтоженный объект.То, что походит на Плохую Вещь, фактически позволяет связанным объектам быть расположенными в непосредственной близости в памяти.Даже если они не помещаются в одну и ту же строку кэша, они, по крайней мере, вписываются в страницу памяти.В некоторой степени более тесная близость может быть достигнута и за счет сжатия сборщика мусора, но в мире C ++ это происходит естественно и определяется уже во время компиляции.
  2. Хотя обычно память просто выделяется и освобождается с помощью операторовnew и delete можно выделять память, например, из пула и организовывать равномерное использование памятью объектов, о которых известно, что они связаны.Это также может быть использовано для размещения объектов в выделенных областях памяти, например, совместно используемой памяти или других диапазонах адресов для специального оборудования.

Хотя эти применения не обязательно используют методы RAII напрямую, они включаютсяболее явный контроль над памятью.Тем не менее, есть также использование памяти, где сборка мусора имеет явное преимущество, например, при передаче объектов между несколькими потоками.В идеальном мире оба метода были бы доступны, и C ++ предпринимает некоторые шаги для поддержки сбора мусора (иногда его называют «сбор мусора»), чтобы подчеркнуть, что он пытается дать бесконечное представление памяти системы, то есть собранные объекты не являютсяуничтожены, но их память используется повторно).Обсуждения пока не идут по пути, принятому C ++ / CLI, с использованием двух разных видов ссылок и указателей.

4 голосов
/ 03 января 2012

Исключения в деструкторах никогда не используются по одной простой причине: деструкторы уничтожают объекты, которые больше не нужны выполняющемуся коду. Любая ошибка, которая происходит во время их освобождения, может безопасно обрабатываться независимо от контекста, например, ведение журнала, отображение пользователю, игнорирование или вызов std::terminate. Окружающий код не заботится, потому что ему больше не нужен объект. Следовательно, вам не нужно распространять исключение через стек и прерывать текущее вычисление.

В вашем примере fp может быть безопасно помещен в глобальную очередь не закрываемых файлов и обработан позже. Код вызова может продолжаться без проблем.

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

4 голосов
/ 03 января 2012

Во-первых: вы не можете сделать что-нибудь полезное с ошибкой, если ваш файловый объект GCed и не может закрыть ФАЙЛ *. Таким образом, оба эквивалентны, насколько это возможно.

Во-вторых, «правильный» шаблон выглядит следующим образом:

class X{
    FILE *fp;
  public:
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }
    ~X(){
        try {
            close();
        } catch (const FileError &) {
            // perhaps log, or do nothing
        }
    }
    void close() {
        if (fp != 0) {
            if(fclose(fp)!=0){
               // may need to handle EAGAIN and EINTR, otherwise
               throw FileError();
            }
            fp = 0;
        }
    }
};

Использование:

X x;
// do stuff involving x that might throw
x.close(); // also might throw, but if not then the file is successfully closed

Если выдает «do stuff», то не имеет значения, был ли дескриптор файла закрыт успешно или нет. Операция не удалась, поэтому файл в любом случае обычно бесполезен. Кто-то наверху цепочки вызовов может знать, что с этим делать, в зависимости от того, как используется файл - возможно, его следует удалить, возможно, оставить в покое в его частично написанном состоянии. Что бы они ни делали, они должны знать, что в дополнение к ошибке, описанной в исключении, которое они видят, возможно, что файловый буфер не был очищен.

RAII используется здесь для управления ресурсами . Файл закрывается несмотря ни на что. Но RAII не используется для определения того, успешно ли выполнена операция - если вы хотите это сделать, вы вызываете x.close(). GC также не используется для определения того, была ли операция успешной, поэтому оба значения равны по этому количеству.

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

Ответ на ваш вопрос - преимущество RAII и причина, по которой вы заканчиваете сбросом или закрытием файловых объектов в предложениях finally в Java, заключается в том, что иногда вы хотите, чтобы ресурс был очищен (насколько это возможно). может быть) немедленно при выходе из области видимости, чтобы следующий бит кода знал, что это уже произошло. Mark-sweep GC не гарантирует этого.

2 голосов
/ 03 января 2012

Q. Когда RAII имеет преимущество перед GC?

A. Во всех случаях, когда ошибки уничтожения неинтересны (т. Е. У вас все равно нет эффективного способа их устранения).

Обратите внимание, что даже при сборке мусора вам придется запускать действие dispose (close, release any) вручную, так что вы можете просто улучшить шаблон RIIA таким же образом:

class X{
    FILE *fp;
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }

    void close()
    {
        if (!fp)
            return;
        if(fclose(fp)!=0){
            throw some_exception();
        }
        fp = 0;
    }

    ~X(){
        if (fp)
        {
            if(fclose(fp)!=0){
                //An error. You're screwed, just throw or std::terminate
            }
        }
    }
}
1 голос
/ 26 сентября 2014

Деструкторы предполагаются всегда успешными.Почему бы просто не убедиться, что fclose не выйдет из строя?

Вы всегда можете сделать fflush или другие действия вручную и проверить ошибку, чтобы убедиться, что fclose будет успешным позже.

...