Понимание значения термина и концепции - RAII (приобретение ресурсов - инициализация)
Не могли бы вы, разработчики C++, дать нам хорошее описание того, что такое RAII, почему он важен и имеет ли он какое-либо отношение к другим языкам?
Я немного знаю Я считаю, что это означает "Приобретение ресурсов - это инициализация". Однако это имя не соответствует моему (возможно, неправильному) пониманию того, что такое RAII: у меня складывается впечатление, что RAII - это способ инициализации объектов в стеке, так что, когда эти переменные выходят из области видимости, деструкторы автоматически вызывать, вызывая очистку ресурсов.
Так почему же это не называется "использование стека для запуска очистки" (UTSTTC:)? Как ты добираешься оттуда до "RAII"?
И как вы можете сделать что-то в стеке, что приведет к очистке чего-то, что живет в куче? Кроме того, есть ли случаи, когда вы не можете использовать RAII? Вы когда-нибудь мечтали собрать мусор? По крайней мере, сборщик мусора, который вы могли бы использовать для некоторых объектов, позволяя управлять другими?
Благодарю.
11 ответов
Так почему же это не называется "использование стека для запуска очистки" (UTSTTC:)?
RAII говорит вам, что делать: приобретите ваш ресурс в конструкторе! Я бы добавил: один ресурс, один конструктор. UTSTTC - это лишь одно из применений, RAII - намного больше.
Управление ресурсами отстой. Здесь ресурс - это все, что требует очистки после использования. Исследования проектов на многих платформах показывают, что большинство ошибок связано с управлением ресурсами - и это особенно плохо в Windows (из-за множества типов объектов и распределителей).
В C++ управление ресурсами особенно сложно из-за комбинации исключений и шаблонов (в стиле C++). Для просмотра под капотом см. GOTW8).
C++ гарантирует, что деструктор вызывается тогда и только тогда, когда конструктор завершился успешно. Полагаясь на это, RAII может решить многие неприятные проблемы, о которых даже может не знать рядовой программист. Вот несколько примеров за пределами "мои локальные переменные будут уничтожены всякий раз, когда я вернусь".
Давайте начнем с чрезмерно упрощенного FileHandle
класс, использующий RAII:
class FileHandle
{
FILE* file;
public:
explicit FileHandle(const char* name)
{
file = fopen(name);
if (!file)
{
throw "MAYDAY! MAYDAY";
}
}
~FileHandle()
{
// The only reason we are checking the file pointer for validity
// is because it might have been moved (see below).
// It is NOT needed to check against a failed constructor,
// because the destructor is NEVER executed when the constructor fails!
if (file)
{
fclose(file);
}
}
// The following technicalities can be skipped on the first read.
// They are not crucial to understanding the basic idea of RAII.
// However, if you plan to implement your own RAII classes,
// it is absolutely essential that you read on :)
// It does not make sense to copy a file handle,
// hence we disallow the otherwise implicitly generated copy operations.
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// The following operations enable transfer of ownership
// and require compiler support for rvalue references, a C++0x feature.
// Essentially, a resource is "moved" from one object to another.
FileHandle(FileHandle&& that)
{
file = that.file;
that.file = 0;
}
FileHandle& operator=(FileHandle&& that)
{
file = that.file;
that.file = 0;
return *this;
}
}
Если конструирование не удается (за исключением), никакая другая функция-член - даже деструктор - не вызывается.
RAII избегает использования объектов в недопустимом состоянии. это уже облегчает жизнь, прежде чем мы даже используем объект.
Теперь давайте посмотрим на временные объекты:
void CopyFileData(FileHandle source, FileHandle dest);
void Foo()
{
CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}
Существует три случая ошибок: ни один файл не может быть открыт, только один файл может быть открыт, оба файла могут быть открыты, но копирование файлов не удалось. В не-RAII реализации, Foo
пришлось бы обрабатывать все три случая явно.
RAII высвобождает ресурсы, которые были получены, даже если несколько ресурсов получены в рамках одного оператора.
Теперь давайте объединим несколько объектов:
class Logger
{
FileHandle original, duplex; // this logger can write to two files at once!
public:
Logger(const char* filename1, const char* filename2)
: original(filename1), duplex(filename2)
{
if (!filewrite_duplex(original, duplex, "New Session"))
throw "Ugh damn!";
}
}
Конструктор Logger
потерпит неудачу, если original
конструктор терпит неудачу (потому что filename1
не может быть открыт), duplex
конструктор терпит неудачу (потому что filename2
не может быть открыт), или запись в файлы внутри Logger
Тело конструктора терпит неудачу. В любом из этих случаев Logger
Деструктор не будет вызван - поэтому мы не можем полагаться на Logger
деструктор для выпуска файлов. Но если original
его деструктор будет вызван во время очистки Logger
конструктор.
RAII упрощает очистку после частичного строительства.
Отрицательные моменты:
Отрицательные моменты? Все проблемы можно решить с помощью RAII и умных указателей;-)
RAII иногда бывает громоздким, когда вам нужно задержать сбор данных, помещая агрегированные объекты в кучу.
Представьте, что Logger нужен SetTargetFile(const char* target)
, В этом случае ручка, которая все еще должна быть членом Logger
, должен находиться в куче (например, в интеллектуальном указателе, чтобы соответствующим образом инициировать уничтожение дескриптора).
Я никогда не мечтал о сборке мусора на самом деле. Когда я делаю C#, я иногда чувствую момент блаженства, который мне просто не нужен, но гораздо больше я скучаю по всем крутым игрушкам, которые могут быть созданы посредством детерминированного разрушения. (с помощью IDisposable
просто не режет.)
У меня была одна особенно сложная структура, которая могла бы выиграть от GC, где "простые" умные указатели вызывали бы циклические ссылки на несколько классов. Мы путаемся, тщательно балансируя сильные и слабые указатели, но всякий раз, когда мы хотим что-то изменить, мы должны изучать большую диаграмму отношений. GC мог бы быть лучше, но некоторые компоненты содержали ресурсы, которые должны быть выпущены как можно скорее.
Примечание к образцу FileHandle: оно не предназначалось для завершения, просто образец - но оказалось неверным. Спасибо Йоханнесу Шаубу за указание и FredOverflow за превращение его в правильное решение C++0x. Со временем я остановился на подходе, задокументированном здесь.
Там есть отличные ответы, поэтому я просто добавляю некоторые забытые вещи.
0. RAII о границах
RAII о обоих:
- получение ресурса (независимо от того, какой ресурс) в конструкторе и удаление его в деструкторе.
- выполнение конструктора при объявлении переменной и автоматическое выполнение деструктора, когда переменная выходит из области видимости.
Другие уже ответили об этом, поэтому я не буду уточнять.
1. При кодировании на Java или C# вы уже используете RAII...
Мсье Журден: Что! Когда я говорю: "Николь, принеси мне мои тапочки и дай мне ночную шапку", это проза?
ФИЛОСОФСКИЙ МАСТЕР: Да, сэр.
Месье Журден: Более сорока лет я говорю прозой, ничего не зная об этом, и я очень благодарен вам за то, что вы научили меня этому.
- Мольер: джентльмен среднего класса, акт 2, сцена 4
Как месье Журден сделал с прозой, C# и даже Java люди уже используют RAII, но скрытно. Например, следующий код Java (который написан так же в C# путем замены synchronized
с lock
):
void foo()
{
// etc.
synchronized(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
... уже использует RAII: получение мьютекса выполняется по ключевому слову (synchronized
или же lock
), и отмена будет сделана при выходе из области видимости.
Это настолько естественно в его обозначениях, что почти не требует объяснений даже для людей, которые никогда не слышали о RAII.
Преимущество C++ перед Java и C# заключается в том, что с помощью RAII можно сделать все что угодно. Например, нет прямого встроенного эквивалента synchronized
ни lock
в C++, но мы все еще можем иметь их.
В C++ было бы написано:
void foo()
{
// etc.
{
Lock lock(someObject) ; // lock is an object of type Lock whose
// constructor acquires a mutex on
// someObject and whose destructor will
// un-acquire it
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
который может быть легко написан способом Java/C# (с использованием макросов C++):
void foo()
{
// etc.
LOCK(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
2. RAII имеют альтернативное использование
БЕЛЫЙ КРОЛИК: [поет] Я опаздываю / опаздываю / На очень важную дату. / Нет времени говорить "Привет". / Прощай. / Я опоздал, я опоздал, я опоздал.
- Алиса в стране чудес (версия Диснея, 1951)
Вы знаете, когда будет вызван конструктор (при объявлении объекта), и знаете, когда будет вызван соответствующий деструктор (при выходе из области видимости), так что вы можете написать почти волшебный код, кроме строки. Добро пожаловать в страну чудес C++ (по крайней мере, с точки зрения разработчика C++).
Например, вы можете написать встречный объект (я позволю это в качестве упражнения) и использовать его, просто объявив его переменную, как был использован объект блокировки выше:
void foo()
{
double timeElapsed = 0 ;
{
Counter counter(timeElapsed) ;
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter's declaration till the scope exit
}
что, конечно, может быть написано, опять же, способом Java/C# с использованием макроса:
void foo()
{
double timeElapsed = 0 ;
COUNTER(timeElapsed)
{
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter's declaration till the scope exit
}
3. Почему C++ не хватает finally
?
[Кричит] Это последний отсчет!
- Европа: последний обратный отсчет (извините, я вышел из кавычек, здесь...:-)
finally
Предложение используется в C#/Java для обработки удаления ресурсов в случае выхода из области (либо через return
или брошенное исключение).
Внимательные читатели спецификаций заметят, что в C++ нет окончательного предложения. И это не ошибка, потому что C++ не нуждается в этом, так как RAII уже обрабатывает удаление ресурсов. (И поверьте мне, написание деструктора C++ намного проще, чем написание правильного предложения Java finally или даже правильного метода Dispose на C#).
Тем не менее, иногда finally
статья была бы крутой. Можем ли мы сделать это в C++? Да мы можем! И снова с альтернативным использованием RAII.
Вывод: RAII - это больше, чем философия в C++: это C++
RAII? ЭТО С ++
- Возмущенный комментарий разработчика C++, беззастенчиво скопированный неизвестным королем Спарты и его 300 друзьями
Когда вы достигаете некоторого уровня опыта в C++, вы начинаете думать с точки зрения RAII, с точки зрения автоматического выполнения конструкторов и деструкторов.
Вы начинаете думать с точки зрения областей, и {
а также }
символы становятся одними из самых важных в вашем коде.
И почти все подходит с точки зрения RAII: безопасность исключений, мьютексы, соединения с базой данных, запросы к базе данных, соединения с сервером, часы, дескрипторы ОС и т. Д., И, наконец, что не менее важно, память.
Часть базы данных не пренебрежимо мала, так как, если вы согласны заплатить цену, вы даже можете написать в стиле "транзакционного программирования", выполняя строки и строки кода до принятия решения, в конце концов, хотите ли вы зафиксировать все изменения или, если это невозможно, возврат всех изменений обратно (при условии, что каждая строка удовлетворяет, как минимум, строгой гарантии исключения). (см. вторую часть этой статьи Херба Саттера о транзакционном программировании).
И как головоломка, все подходит.
RAII - это большая часть C++, C++ не может быть C++ без него.
Это объясняет, почему опытные разработчики C++ так очарованы RAII, и почему RAII - это первое, что они ищут, когда пытаются использовать другой язык.
И это объясняет, почему сборщик мусора, хотя сам по себе является важной технологией, не так впечатляет с точки зрения разработчика C++:
- RAII уже обрабатывает большинство дел, обработанных GC
- GC работает лучше, чем RAII, с циклическими ссылками на чисто управляемые объекты (смягчается умным использованием слабых указателей)
- Тем не менее, GC ограничен памятью, а RAII может обрабатывать любые виды ресурсов.
- Как описано выше, RAII может многое, намного больше...
RAII использует семантику деструкторов C++ для управления ресурсами. Например, рассмотрим умный указатель. У вас есть параметризованный конструктор указателя, который инициализирует этот указатель с адресом объекта. Вы выделяете указатель на стек:
SmartPointer pointer( new ObjectClass() );
Когда умный указатель выходит из области видимости, деструктор класса указателя удаляет связанный объект. Указатель размещается в стеке, а объект - в куче.
Есть определенные случаи, когда RAII не помогает. Например, если вы используете умные указатели подсчета ссылок (например, boost::shared_ptr) и создаете графоподобную структуру с циклом, вы рискуете столкнуться с утечкой памяти, поскольку объекты в цикле будут препятствовать освобождению друг друга. Сборка мусора поможет против этого.
Я хотел бы выразить это немного сильнее, чем предыдущие ответы.
RAII, Resource Acquisition Is Initialization означает, что все полученные ресурсы должны быть получены в контексте инициализации объекта. Это запрещает "голое" приобретение ресурсов. Обоснование состоит в том, что очистка в C++ работает на основе объекта, а не на основе вызова функции. Следовательно, вся очистка должна выполняться объектами, а не вызовами функций. В этом смысле C++ более объектно-ориентирован, чем, например, Java. Очистка Java основана на вызовах функций в finally
статьи.
Я согласен с cpitis. Но хотелось бы добавить, что ресурсы могут быть чем угодно, а не только памятью. Ресурсом может быть файл, критический раздел, поток или соединение с базой данных.
Это называется Resource Acquisition Is Initialization, потому что ресурс получается, когда создается объект, управляющий ресурсом. Если конструктор вышел из строя (т.е. из-за исключения), ресурс не был получен. Затем, когда объект выходит из области видимости, ресурс освобождается. C++ гарантирует, что все объекты в стеке, которые были успешно построены, будут разрушены (это включает в себя конструкторы базовых классов и членов, даже если конструктор суперкласса завершится неудачно).
Рациональное обоснование RAII состоит в том, чтобы сделать исключение для получения ресурсов безопасным. Чтобы все полученные ресурсы были освобождены должным образом, независимо от того, где происходит исключение. Однако это зависит от качества класса, который получает ресурс (это должно быть исключение безопасно, и это трудно).
Проблема со сборкой мусора состоит в том, что вы теряете детерминированное уничтожение, которое имеет решающее значение для RAII. Как только переменная выходит из области видимости, это зависит от сборщика мусора, когда объект будет возвращен. Ресурс, который удерживается объектом, будет продолжать удерживаться до вызова деструктора.
RAII происходит от Распределение ресурсов - Инициализация. По сути, это означает, что когда конструктор заканчивает выполнение, построенный объект полностью инициализируется и готов к использованию. Это также подразумевает, что деструктор освободит любые ресурсы (например, память, ресурсы ОС), принадлежащие объекту.
По сравнению с языками / технологиями сбора мусора (например, Java, .NET), C++ позволяет полностью контролировать жизнь объекта. Для объекта, размещенного в стеке, вы будете знать, когда будет вызван деструктор объекта (когда выполнение выходит из области видимости), что не контролируется при сборе мусора. Даже используя умные указатели в C++ (например, boost::shared_ptr), вы будете знать, что когда нет ссылки на указанный объект, будет вызван деструктор этого объекта.
И как вы можете сделать что-то в стеке, что приведет к очистке чего-то, что живет в куче?
class int_buffer
{
size_t m_size;
int * m_buf;
public:
int_buffer( size_t size )
: m_size( size ), m_buf( 0 )
{
if( m_size > 0 )
m_buf = new int[m_size]; // will throw on failure by default
}
~int_buffer()
{
delete[] m_buf;
}
/* ...rest of class implementation...*/
};
void foo()
{
int_buffer ib(20); // creates a buffer of 20 bytes
std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.
Когда экземпляр int_buffer появляется, он должен иметь размер и выделять необходимую память. Когда он выходит из области видимости, вызывается его деструктор. Это очень полезно для таких вещей, как объекты синхронизации. Рассматривать
class mutex
{
// ...
take();
release();
class mutex::sentry
{
mutex & mm;
public:
sentry( mutex & m ) : mm(m)
{
mm.take();
}
~sentry()
{
mm.release();
}
}; // mutex::sentry;
};
mutex m;
int getSomeValue()
{
mutex::sentry ms( m ); // blocks here until the mutex is taken
return 0;
} // the mutex is released in the destructor call here.
Кроме того, есть ли случаи, когда вы не можете использовать RAII?
Нет, не совсем.
Вы когда-нибудь мечтали собрать мусор? По крайней мере, сборщик мусора, который вы могли бы использовать для некоторых объектов, позволяя управлять другими?
Никогда. Сборка мусора решает только очень небольшое подмножество динамического управления ресурсами.
Здесь уже есть много хороших ответов, но я просто хотел бы добавить:
Простое объяснение RAII состоит в том, что в C++ объект, размещенный в стеке, уничтожается всякий раз, когда он выходит из области видимости. Это означает, что деструктор объектов будет вызван и сможет выполнить всю необходимую очистку.
Это означает, что если объект создается без "new", "delete" не требуется. И это также идея "умных указателей" - они находятся в стеке и по существу обертывают объект на основе кучи.
RAII является аббревиатурой "Приобретение ресурсов - инициализация".
Этот метод очень уникален для C++, поскольку он поддерживает как конструкторы и деструкторы, так и почти автоматически конструкторы, которые соответствуют передаваемым аргументам, или наихудший случай, когда конструктор по умолчанию называется & destructors, если предоставленная простота вызывается, иначе - по умолчанию то, что добавляется компилятором C++, вызывается, если вы явно не написали деструктор для класса C++. Это происходит только для объектов C++, которые управляются автоматически - это означает, что они не используют свободное хранилище (память выделяется / освобождается с помощью операторов new,new[]/delete,delete[] C++).
Метод RAII использует эту функцию автоматического управления объектами для обработки объектов, созданных в куче / свободном хранилище, явно запрашивая дополнительную память, используя new/new[], которая должна быть явно уничтожена вызовом delete/delete[], Класс автоматически управляемого объекта обернет этот другой объект, созданный в памяти heap / free-store. Следовательно, когда запускается конструктор автоматически управляемого объекта, обернутый объект создается в памяти кучи / свободного хранилища, а когда дескриптор автоматически управляемого объекта выходит из области видимости, автоматически вызывается деструктор этого автоматически управляемого объекта, в который переносится объект объект уничтожен с помощью delete. С концепциями ООП, если вы поместите такие объекты в другой класс в закрытой области видимости, у вас не будет доступа к обернутым классам-членам и методам, и это является причиной, по которой предназначены интеллектуальные указатели (иначе говоря, классы дескрипторов). Эти интеллектуальные указатели представляют обернутый объект как типизированный объект для внешнего мира и позволяют вызывать любые члены / методы, из которых состоит объект памяти. Обратите внимание, что умные указатели имеют различные вкусы в зависимости от потребностей. Чтобы узнать больше об этом, вам следует обратиться к программированию на современном C++ Андрея Александреску или повысить уровень реализации (документации) библиотеки (www.boostorg) shared_ptr.hpp. Надеюсь, это поможет вам понять RAII.