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

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

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

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


person hyperman    schedule 03.01.2012    source источник
comment
Разве у вас нет такой же проблемы в finally-разделе, например, в Java?   -  person Niklas B.    schedule 03.01.2012
comment
один из примеров, который приходит на ум (не очень релевантный для вашего примера, но ...) - это средства защиты блокировки: boost.org/doc/libs/1_48_0/doc/html/thread/   -  person Anycorn    schedule 03.01.2012
comment
При неправильном использовании сборщиков мусора для общего управления ресурсами (например, дескрипторов файлов) возникает точно такая же проблема. Куда сборщик мусора должен выдавать исключения? Интересующего кода давно нет.   -  person thiton    schedule 03.01.2012
comment
Просто для полноты: в вашем примере отсутствует ctor копирования и оператор присваивания.   -  person pmr    schedule 03.01.2012


Ответы (8)


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

Теперь перейдем к реальной проблеме, которая у вас возникла. Это обсуждение возникло недавно в чате Lounge ‹C ++› о том, что делать, если деструктор объекта RAII может дать сбой.

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

person Xeo    schedule 03.01.2012
comment
На данный момент это кажется наиболее реалистичным ответом на мой вопрос. В приложениях RT я бы с вами согласился. Однако большинство стандартных приложений могут время от времени работать с замедлением на несколько миллисекунд. - person hyperman; 03.01.2012
comment
@ user844382: какие миллисекунды? Задержка перед сборкой объекта в сборку может составлять часы, если приложение не выделяет ресурсы. Нет никакого замедления - с GC mark-sweep приложение продолжает работать, а финализатор может или не может быть выполнен через некоторое время. - person Steve Jessop; 03.01.2012
comment
@user: Именно так и говорит Стив. Если нет необходимости освобождать пространство ресурсов, GC никогда даже не рассмотрит возможность освобождения вашего ресурса (кроме случаев, когда об этом явно сказано GC.Collect()). - person Xeo; 03.01.2012
comment
@ user844382: У Xeo есть куда более важный момент. Детерминизм - это не преимущество в скорости, а воспроизводимость ошибок. У вас есть приложение Java, в котором происходит утечка памяти? Вы никогда не найдете цикл указателя или ошибку сборщика мусора, которая вызвала его, потому что сборщик мусора делает свое дело невоспроизводимо. У вас есть приложение на C ++, отличное от GC, с той же проблемой? Запустите его один раз под valgrind, и вы получите ошибку. - person thiton; 03.01.2012
comment
@thiton Это не совсем так. Обнаружить утечки памяти в java-приложениях - тривиально. Через некоторое время сделайте снимок кучи, дайте приложению поработать какое-то время, сделайте новый снимок, сравните. Вуаля, вы легко найдете хэш-карту (обычный кандидат) или любую другую структуру, в которой хранятся ссылки. valgrind - это хорошо, но, к сожалению, не работает для всех приложений (и не только для каких-либо окон, точка доступа также имеет тенденцию перегружать valgrind). А насчет ошибок горячих точек: ну, это приложение на c, поэтому лучше не заявлять, что невозможно найти утечки памяти в c / c ++;) - person Voo; 03.01.2012
comment
Дело в том, что сборщик мусора предназначен для управления memory НЕ системными ресурсами. С помощью нескольких хаков это можно сделать (например, запустить все финализаторы при завершении работы), но он не был создан для этого и не должен использоваться таким образом. Я не хочу запускать сборщик мусора каждый раз, когда системный ресурс становится скудным, в смутной надежде, что финализатор выпустит какие-то вещи (не говоря уже о замедлении работы всего сборщика мусора при использовании большого количества финализаторов) - person Voo; 03.01.2012
comment
@Steve Jessop Но RAII также может закончиться запуском через несколько часов или никогда, если объект принадлежит shared_ptr или если поток управления таков, что объект покидает область видимости. Любой объект, который не входит в область видимости или не имеет ссылок на него (т.е. случаи, когда область видимости или shared_ptr вызывает деструктор) всегда будет освобожден сборщиком мусора при полном запуске. Часто работающий сборщик мусора почти всегда очищает долгоживущие объекты быстрее, чем методы на основе RAII. - person saolof; 04.06.2018
comment
@saolof: Оставляя в стороне ссылочные циклы, RAII разрушает объект именно в тот момент, когда он становится пригодным для сборки мусора. GC не может освободить его раньше. С учетом отсоединенных циклов ссылок, ОК, сборщик мусора может освободить объект при следующем запуске, что раньше, чем это делает RAII: освободить его никогда. Но этот ответ касается детерминизма RAII, и факт остается фактом, что RAII детерминирован. Вероятно, не то, что вы намеревались, но детерминированно так. - person Steve Jessop; 18.07.2018
comment
@Steve Jessop RAII уничтожает объект именно в тот момент, когда он становится пригодным для сборки мусора. Нет, это не так. У вас может быть что-то вроде void foo () {hugetype x = makeHugeObject (); бар (х); doLongOperation (); } Где x может быть восстановлен до или во время выполнения бита doLongOperation. С RAII x будет без необходимости присутствовать на всех вычислениях doLongOperation и потреблять память. Это особенно плохо в рекурсивных алгоритмах, где RAII может просочиться, а GC - нет. Также возможно разработать сборщик мусора, который всегда будет выполнять очистку раньше, чем RAII, в каждом отдельном случае. - person saolof; 30.07.2018
comment
@saolof: Я немного забыл об этом, но я думаю, что в C ++ x не подходит для сборки мусора во время doLongOperation(). Имейте в виду, что его уничтожение может иметь наблюдаемые побочные эффекты, которые реализация не может изменить порядок относительно doLongOperation(). Очевидно, это своего рода циклическое определение: RAII уничтожает объект именно тогда, когда он имеет право на уничтожение, потому что по замыслу RAII проигрывает правила времени существования объекта C ++, которые говорят, что объект имеет право на уничтожение в конце области, в которой использовался RAII. Но это определение у нас есть. - person Steve Jessop; 01.12.2018
comment
И, конечно, если было достаточно встраивания, чтобы доказать отсутствие наблюдаемой разницы, RAII или GC в равной степени имеют право использовать правило as-if для раннего уничтожения x. Оба могут сделать вывод, что в тот же определенный момент мы можем рассматривать x как выходящую за рамки, поскольку он больше никогда не используется. - person Steve Jessop; 01.12.2018
comment
Сборщик мусора собирает мусор во время выполнения. Он может собираться в любое время во время работы doLongOperation (). Ему не нужно ничего знать об областях видимости, и он не заботится о встраивании. Если doLongOperation () достаточно большой, что очень вероятно хотя бы одна сборка gc во время ее работы, тогда GC почти всегда очистит это быстрее. В то время как RAII должен ждать, пока область действия объекта не закончится, прежде чем он сможет собрать, что может быть сколь угодно долгим и обычно блокирует оптимизацию хвостового вызова. - person saolof; 03.01.2020

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

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

person Ernest Friedman-Hill    schedule 03.01.2012
comment
Вы правы, когда говорите, что сборщик мусора также не решает проблему без памяти, и мой вопрос можно было бы сформулировать лучше. Попытка перефразировать это немного лучше: RAII не лучше в управлении ресурсами, чем GC, и требует от вас написания (и особенно: отладки) большого количества кода. Итак, когда бы вам действительно было лучше использовать RAII? - person hyperman; 03.01.2012
comment
Возможно, аргумент Б.Строуступа, почему GC еще не реализован в стандарте, частично ответит на этот вопрос - см. «Развитие языка в реальном мире и для него: C ++ 1991-2006, гл. 5.4 Автоматический сбор мусора (www2.research.att.com/~bs /hopl-almost-final.pdf) - person SChepurin; 03.01.2012

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

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

person Puppy    schedule 03.01.2012
comment
Значит, допустимо распечатать ошибку и выйти? Или лучше проигнорировать (возможно, после регистрации)? - person Niklas B.; 03.01.2012
comment
Ваш второй абзац немного преувеличивает, ИМО, так как я могу представить множество причин, по которым может произойти сбой общего освобождения ресурсов, которые не являются сценариями конца света. Если диск заполняется и вы не можете закрыть файл, вы уведомите пользователя и повторите попытку позже. - person Ernest Friedman-Hill; 03.01.2012
comment
Я не думаю, что это всегда правда. Например, отказ от удаления временного файла не обязательно должен приводить к завершению работы приложения. - person StackedCrooked; 03.01.2012
comment
@NiklasBaumstark Игнорировать его (да, возможно, после регистрации). fclose всегда закрывает файл, даже если он не может записать все данные, которые еще не были записаны на диск, поэтому с точки зрения ресурсов ошибки не возникает. - person ; 03.01.2012
comment
@hvd: Но, надо признать, есть ошибка приложения. Вызывающий ожидает, что его данные будут записаны, и если мы заставим его flush раньше, мы могли бы также заставить его close и вообще не использовать RAII. - person Niklas B.; 03.01.2012
comment
@NiklasBaumstark Это не одно и то же: fclose следует вызывать, даже если есть исключение на полпути во время записи данных. Но если есть исключение на полпути записи данных, вам все равно, удастся ли fclose, потому что данные в любом случае неверны. - person ; 03.01.2012
comment
@hvd: Ах, в этом есть смысл. Таким образом, разница в том, что flush не нужно вызывать для выхода в согласованное состояние, а fclose нужно вызывать. Спасибо. - person Niklas B.; 03.01.2012
comment
@hvd Игнорировать это недопустимо. Представьте, что вы пишете текст, сохраняете его и обнаруживаете, что последние несколько килобайт не сохранены. С точки зрения ресурсов ошибок нет, но я хотел бы, чтобы моя программа сообщила мне об этом раньше. - person hyperman; 03.01.2012
comment
@ user844382 Вот почему отдельный метод flush () может вызвать исключение. - person ; 03.01.2012

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

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

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

person thiton    schedule 03.01.2012
comment
Цель деструктора не в том, чтобы уничтожить то, что больше не нужно. Цель состоит в том, чтобы сделать любые ресурсы, которые использовал объект (память, файлы, дескрипторы GDI или что-то еще), доступными для будущего использования другими объектами. Если в подпрограмме есть автоматическая переменная Foo типа класса Bar, эта переменная перестанет существовать при выходе из подпрограммы, и деструктору ничего не нужно будет делать. Цель деструктора не в том, чтобы изменить что-то, что перестанет существовать, а в том, чтобы манипулировать внешними объектами, которые должны использоваться без уничтоженного объекта. - person supercat; 23.03.2012

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

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

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(). Сборщик мусора также не используется для определения успешности операции, поэтому по этому счету они равны.

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

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

person Steve Jessop    schedule 03.01.2012
comment
В качестве практического решения согласен. Однако вы фактически подтверждаете мою точку зрения: деструктор здесь не помогает программисту. Да, файл закрыт или мьютекс уничтожен, но когда что-то идет не так, происходит тихое повреждение файла, странная тупиковая ситуация и т. Д. Я предпочитаю, чтобы мое приложение вылетало в этот момент. - person hyperman; 03.01.2012
comment
@ user844382: Я думал, вы задаете вопрос: ваш блог предназначен для того, чтобы зарабатывать очки. Деструктор действительно помогает программисту, он обеспечивает вызов fclose в FILE*. Он не делает всего того, что может захотеть сделать программист. Если ваша точка зрения заключалась в том, что GC делает все (даже если он делает все, что делает RAII), то ваша точка зрения просто неверна. Если вы действительно хотите, чтобы приложение вылетало из строя, вы можете вызвать abort или разыменовать нулевой указатель вместо того, чтобы вести журнал в указанной мной точке. - person Steve Jessop; 03.01.2012
comment
Да, и о повреждении файла ничего не говорится, только если вы ошибочно предполагаете, что файл записан без успешного вызова close. Так что не делайте этого, равно как и предположите, что файл записан без предварительного вызова fwrite для фактической записи некоторых данных. Что касается мьютексов - я не знаю, при каких обстоятельствах освобождение ваших мьютексов не удается, но я бы предложил исправить окружающий код, поскольку единственные задокументированные сбои (например) pthread_mutex_unlock заключаются в том, что ввод не является допустимым мьютексом или не принадлежит вызывающему. - person Steve Jessop; 03.01.2012

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

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

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

person Dietmar Kühl    schedule 03.01.2012

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

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

Обратите внимание, что даже со сборкой мусора вам придется вручную запустить действие 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
            }
        }
    }
}
person sehe    schedule 03.01.2012
comment
Это почти то, что я собирался опубликовать: добавить метод flush (). Если fclose в деструкторе не работает, нет, вы не облажались. Файл закрыт, но некоторые данные могли не быть сохранены. Если вызывающий хочет исключение для этого, вызывающий должен сначала вызвать flush (). - person ; 03.01.2012
comment
Ваше первое предложение не приговор. - person Niklas B.; 03.01.2012
comment
@hvd: ну, я имел в виду you're screwed в целом, поскольку это явно намерение ОП. Для этого конкретного деструктора да, вы правы. - person sehe; 03.01.2012
comment
@NiklasBaumstark: спасибо, что сообщили об этом. Тем не менее, ответ на этот вопрос имеет много смысла. - person sehe; 03.01.2012
comment
@sehe Применяется ко всем деструкторам. Если ваш класс содержит ресурсы, он может освобождать их без исключения, в том числе и в других случаях, упомянутых в вопросе. - person ; 03.01.2012
comment
@hvd: нет, не обязательно безопасно. Да, вы можете принимать исключения, если хотите. Я бы точно не рекомендовал это. (Пример: невыполнение семафора или мьютекса должно быть основанием для завершения операции, в противном случае следует UB, потенциально нарушающий синхронизацию потоков или приводящий к тупиковой ситуации). - person sehe; 03.01.2012
comment
@sehe Ваш класс должен отслеживать, действителен ли мьютекс, и проверять это в деструкторе. Если он действителен, то освобождение мьютекса не может завершиться ошибкой. Это гарантировано в pthreads, а также, вероятно, в других реализациях. - person ; 03.01.2012

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

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

person Community    schedule 26.09.2014