Подтвердить что ты не робот

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

Рассмотрим этот простой класс, демонстрирующий RAII в С++ (сверху):

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-потоков, графических ресурсов... Я отмечаю, как, например, страница wikipedia RAII охватывает всю проблему под ковриком: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

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

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

4b9b3361

Ответ 1

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

Теперь о реальной проблеме, которая вам кажется. Это обсуждение появилось в Lounge < С++ > чат-комнату некоторое время назад о том, что вы должны делать, если деструктор объекта RAII может потерпеть неудачу.

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

Во-первых: вы не можете ничего сделать с ошибкой, если ваш файл-объект 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

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

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Обратите внимание, что даже при сборке мусора вам нужно будет вручную запустить действие "dispose" (закрыть, освободить), чтобы вы могли просто улучшить шаблон 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
            }
        }
    }
}

Ответ 8

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

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