Описание тега raii
Приобретение ресурсов - это инициализация (RAII) - это идиома программирования, используемая в нескольких объектно-ориентированных языках, в первую очередь в C++, где она возникла, а также в D, Ada, Vala и Rust. Этот метод был разработан для безопасного управления ресурсами в C++ в течение 1984–1989 годов, в основном Бьярном Страуструпом и Эндрю Кенигом, а сам термин был придуман Страуструпом.
Базовый пример
Самый простой пример RAII может выглядеть так:
struct intarray {
explicit intarray (size_t size) : array(new int[size]) {}
~intarray (){ delete[] array; }
int& operator[](size_t idx) { return array[idx]; }
const int& operator[] const (size_t idx) { return array[idx]; }
private:
intarray (const intarray &);
intarray & operator=(const intarray&);
int* const array;
};
Этот класс инкапсулирует выделение памяти (для массива int
), так что когда intarray
создается класс, он создает ресурс (выделение памяти), и когда он уничтожается, он уничтожает ресурс.
Теперь пользователь класса может написать такой код для создания массива, размер которого определяется во время выполнения:
void foo(size_t size) {
intarray myarray(size);
// no bounds checking in this example: assume that size is at least 15
myarray[10] = 0;
myarray[14] = 123;
}
Этот код не приведет к утечке памяти.myarray
объявлен в стеке, поэтому он автоматически уничтожается, когда мы выходим из области видимости, в которой он был объявлен; когдаfoo
возвращается, myarray
вызывается деструктор. А такжеmyarray
деструктор изнутри вызывает delete[]
на внутреннем массиве.
Мы можем написать более тонкую версию функции, которая также не будет пропускать память:
void foo(size_t size) {
intarray myarray(size);
bar();
}
Мы ничего не знаем о bar
(хотя мы предполагаем, что утечки памяти не произойдет), поэтому возможно, что это может вызвать исключение. Потому какmyarray
является локальной переменной, она по- прежнему автоматически уничтожается, и поэтому неявно освобождает выделенную память, за которую она была ответственна.
Без использования RAII нам пришлось бы написать что-то вроде этого, чтобы избежать утечек памяти:
void foo(size_t size) {
int* myarray = new int[size];
try {
bar();
delete[] myarray;
}
catch (...){
delete[] myarray;
throw;
}
}
Полагаясь на RAII, нам больше не нужно писать неявный код очистки. Мы полагаемся на автоматическое время жизни локальных переменных для очистки связанных с ними ресурсов.
Обратите внимание, что в этом простом примере были объявлены конструктор копирования и оператор присваивания. private
, давая нам класс, который нельзя скопировать. Если бы это не было сделано, при их реализации необходимо было бы позаботиться о том, чтобы ресурс был освобожден только один раз. (Наивный конструктор копирования просто скопирует указатель массива из исходного объекта, в результате чего два объекта будут содержать указатель на одно и то же выделение памяти, и поэтому оба будут пытаться освободить его при вызове своих деструкторов).
Распространенными решениями являются либо использование схемы подсчета ссылок (чтобы последний удаляемый объект также был тем, кто окончательно удалял общий ресурс), либо просто клонирование ресурса при копировании оболочки RAII.
RAII в стандартной библиотеке
RAII широко используется в стандартной библиотеке C++. Классы контейнеров, такие какstd::vector
использовать RAII для управления сроком жизни хранимых в них объектов, чтобы пользователю не приходилось отслеживать выделенную память. Например,
std::vector<std::string> vec;
std::string hello = "hello";
vec.push_back(hello);
содержит множество выделений памяти:
- вектор размещает внутренний массив, концептуально аналогично
intarray
класс, описанный выше, - создается строка, содержащая динамически выделяемый буфер, в котором хранится текст "привет",
- вторая строка выделяется во внутреннем буфере вектора, и эта строка также создает внутренний буфер для хранения своих данных. Данные из первой строки копируются во вторую строку.
И все же, как пользователи библиотеки, нам не пришлось звонить new
хотя бы один раз, и нам не нужно звонить delete
или беспокоиться об очистке. Мы создали наши объекты в стеке, где они автоматически уничтожаются, когда выходят за пределы области видимости, и заботятся о распределении своей памяти внутри. Даже когда мы копируем из одной строки в другую,string
реализация заботится о копировании внутренних буферов, так что каждая строка владеет отдельной копией строки"hello"
.
И когда вектор выходит за пределы области видимости, он заботится об уничтожении каждого хранящегося в нем объекта (который, в свою очередь, отвечает за освобождение его внутренней памяти) перед освобождением своего внутреннего буфера.
Стандартная библиотека также использует RAII для управления другими ресурсами, такими как дескрипторы файлов. Когда мы создаем файловый поток, это класс-оболочка RAII, который становится владельцем дескриптора файла, используемого внутри. Поэтому, когда файловый поток уничтожается, дескриптор файла также закрывается и уничтожается, что позволяет нам писать такой код:
void foo() {
std::ofstream("file.txt") << "hello world";
}
Опять же, мы создаем объект (в данном случае поток выходного файла), который внутренне выделяет один или несколько ресурсов (он получает дескриптор файла и, скорее всего, также выполняет одно или несколько выделений памяти для внутренних буферов и вспомогательных объектов), и как пока объект находится в области видимости, мы его используем. Как только он выходит из области видимости, он автоматически очищает каждый полученный ресурс.
Умные указатели
Многие люди приравнивают RAII к использованию общих классов интеллектуальных указателей, что является чрезмерным упрощением. Как и в предыдущих двух примерах, RAII можно использовать во многих случаях, не полагаясь на класс интеллектуального указателя.
Интеллектуальный указатель - это объект с тем же интерфейсом, что и указатель (иногда с небольшими ограничениями), но который становится владельцем объекта, на который он указывает, так что интеллектуальный указатель берет на себя ответственность за удаление объекта, на который он указывает.
Библиотека Boost содержит несколько широко используемых интеллектуальных указателей:
boost::shared_ptr<T>
реализует подсчет ссылок, поэтому многиеshared_ptr
может указывать на объект типаT
, последний, который будет уничтожен, отвечает за удаление указанного объекта.boost::scoped_ptr<T>
имеет некоторое сходство сintarray
Например, это указатель, который нельзя скопировать или присвоить. Ему дается право собственности на объект при создании, и он, когда выходит за пределы области видимости, уничтожает этот объект.
Стандартная библиотека C++ содержит std::auto_ptr<T>
(заменено в C++11 на std::unique_ptr<T>
), который, как и scoped_ptr
позволяет только один указатель на владение объектом, но в отличие от него, и это позволяет собственности быть переданы, так что оригинальный указатель теряет право собственности на объект, и новый указатель выгоды его. Исходный указатель затем становится нулевым указателем, который ничего не делает при уничтожении.