Когда 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;
}
Я не могу бросить исключение в деструкторе. У меня есть ошибка, но нет способа сообщить об этом. И этот пример довольно общий: я могу сделать это не только с файлами, но и с e.G потоки posix, графические ресурсы, ... Я отмечаю, как, например, страница Википедии RAII подметает всю проблему под ковром: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
Мне кажется, что RAII полезен только в том случае, если уничтожение гарантированно произойдет без ошибок. Единственные ресурсы, известные мне с этим свойством, - это память. Теперь мне кажется, что, например, Бем довольно убедительно развенчивает идею ручного управления памятью-хорошая идея в любой общей ситуации, так где же преимущество в способе использования RAII на C++?
Да, я знаю, GC немного еретичен в мире c++; -)
8 ответов
RAII, в отличие от GC, является детерминированные. Вы будете точно знать, когда ресурс будет выпущен, в отличие от "когда-нибудь в будущем он будет выпущен", в зависимости от того, когда GC решит, что ему нужно снова запустить.
теперь о реальной проблеме, которую вы, похоже, имеете. Это обсуждение появилось в чате Lounge
вывод был, что лучший способ было бы предоставить конкретный close()
, destroy()
или аналогичная функция-член, которая вызывается деструктором, но также может быть вызвана до этого, если вы хотите обойти проблему "исключение во время размотки стека". Затем он установит флаг, который остановит его от вызова в деструкторе. std::(i|o)fstream
например, делает именно это - он закрывает файл в своем деструкторе, но также предоставляет close()
метод.
Это аргумент соломенного человека, потому что вы не говорите о сборе мусора (освобождение памяти), вы говорите об общем управлении ресурсами.
Если вы неправильно использовали сборщик мусора для закрытия файлов таким образом, тогда у вас будет идентичная ситуация: вы также не можете создать исключение. Те же варианты будут открыты для вас: игнорирование ошибки или, что гораздо лучше, ее регистрация.
точно такая же проблема возникает при сборе мусора.
однако стоит отметить, что если в вашем коде нет ошибки или в коде библиотеки, который питает ваш код, удаление ресурса должно никогда провал. delete
никогда не терпит неудачу, если вы не повредили свою кучу. Это одна и та же история для каждого ресурса. Неспособность уничтожить ресурс-это завершающий сбой приложения, а не приятное исключение "обработайте меня".
исключения в деструкторах никогда не полезны по одной простой причине: деструкторы уничтожают объекты, которые больше не нужны запущенному коду. Любая ошибка, которая происходит во время их освобождения, может быть безопасно обработана в контексте-агностическим способом, таким как ведение журнала, отображение пользователю, игнорирование или вызов std::terminate
. Окружающему коду все равно, потому что он больше не нуждается в объекте. Поэтому вам не нужно распространять исключение через стек и прерывать текущий вычисление.
в вашем примере fp
может быть безопасно помещен в глобальную очередь не закрываемых файлов и обработан позже. Вызывающий код может продолжаться без проблем.
по этому аргументу деструкторам очень редко приходится бросать. На практике они действительно редко бросают, объясняя широкое использование RAII.
во-первых: вы не можете сделать ничего полезного с ошибкой, если ваш файловый объект 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, это то, что иногда вы хотите, чтобы ресурс был очищен (насколько это возможно) сразу после выхода из области, так что следующий бит кода знает, что это уже произошло. Марк-sweep GC не гарантирует этого.
Я хочу добавить еще несколько мыслей, касающихся "RAII" против GC. Аспекты использования какой-то функции close, destroy, finish уже объясняются как аспект детерминированного высвобождения ресурсов. Есть, по крайней мере, еще два важных средства, которые активируются с помощью деструкторов и, таким образом, отслеживают ресурсы программистским способом:
- в мире RAII можно иметь устаревший указатель, т. е. указатель, который указывает на уже уничтоженный объект. То, что звучит как плохая вещь, на самом деле позволяет связанным объектам находиться в непосредственной близости в памяти. Даже если они не вписываются в одну и ту же кэш-строку, они, по крайней мере, вписываются в страницу памяти. В какой-то степени более тесная близость может быть достигнута и уплотнительным сборщиком мусора, но в мире C++ это происходит естественно и определяется уже во время компиляции.
- хотя обычно память просто выделяется и освобождается с помощью операторы
new
иdelete
возможно выделить память например от бассеина и аранжировать для Даже пользы памяти compacter известных, что было связано. Это также может использоваться для размещения объектов в выделенных областях памяти, например, общая память или другие диапазоны адресов для специального оборудования.
хотя эти использования не обязательно используют методы RAII напрямую, они включены более явным контролем над памятью. Тем не менее, есть также использование памяти, где мусор коллекция имеет явное преимущество, например, при передаче объектов между несколькими потоками. В идеальном мире оба метода были бы доступны, и C++ предпринимает некоторые шаги для поддержки сбора мусора (иногда называемый "сборкой мусора", чтобы подчеркнуть, что он пытается дать бесконечное представление о памяти системы, т. е. собранные объекты не уничтожаются, но их местоположение памяти используется повторно). Обсуждения до сих пор не следуют маршруту, выбранному C++/CLI использования двух разных видов ссылки и указатели.
Q. когда 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
}
}
}
}
деструкторы считаются всегда успешными. Почему бы просто не убедиться, что fclose
не получится?
вы всегда можете сделать fflush
или некоторые другие вещи вручную и проверьте ошибку, чтобы убедиться, что fclose
получится позже.