Идиома RAII — захват ресурса есть инициализация
RAII – идиома, получившая широкое распространение благодаря создателю С++ Бьярну Страуструпу, и расшифровывается как “Resource Acquisition is Initialization” – захват ресурса есть инициализация.
Идиома очень простая и кратко описывается следующим образом: в конструкторе объект получает доступ к какому либо ресурсу (например, открывается файл или устанавливается соединение по сети к базе данных) и сохраняет описатель ресурса в закрытый члена класса, а при вызове деструктура этот ресурс освобождается (закрывается файл или соединение к БД). При объявлении объекта данного класса на стеке происходит и его инициализация с вызовом конструктора, захватывающий ресурс. При выходе из области видимости объект выталкивается из стека, но перед этим вызывается деструктор объекта, который и освобождает захваченный ресурс.
Например, если мы хотим поговорить с кем-то по телефону, то при инициализации разговора мы снимаем трубку, при этом захватываем ресурс под названием “Телефонная Линия”. Вдоволь наговорившись с собеседником, мы прерываем разговор и кладем трубку и одновременно освобождаем телефонную линию. Попробуем написать это на C++. Сначала определим класс, который будет символизировать ресурс, который мы захватываем. Класс не имеет состояния и содержит всего две функции-члена: pickThePhoneUp() – поднимает трубку и занимает телефонную линию, и putThePhoneDown() – кладет трубку и освобождает линию:
Далее напишем класс, который будет описывать телефонный разговор. Класс содержит один закрытый член, который является экземпляром класса TelephoneLine и описывает состояние телефонной линии. Это и есть ресурс, который мы захватываем при создании, и освобождаем при уничтожении объекта:
Обработка возможных исключений при создании объекта класса TelephoneLine здесь намеренно опущена для того, чтобы не загромождать пример. Копирующий конструктор и оператор копирующего присваивания (строки 15 и 16) намеренно сделаны закрытыми и оставлены без реализации для того, чтобы предотвратить копирование объекта, так как в подобном случае получится что уже два и более объекта обладают одним ресурсом, а это может привести к непредсказуемым последствиям.
Теперь воспользуемся написанными классами:
Фигурные скобки в строке 3 и 7 используются для ограничения области видимости объекта, описывающего телефонный разговор — call. При создании объекта call происходит поднятие телефонной трубки и захват линии. При выходе из области видимости, ограниченной фигурными скобками, объект call уничтожается, при этом автоматически происходит вызов функции-члена putThePhoneDown() из деструктора, и, таким образом, ресурс “Телефонная линия” освобождается.
После запуска мы увидим следующий вывод:
Let's make a call to a friend.
Line locked
Oh, we've talked enough. I need to take a nap. Goodbye!
Line unlocked
Zzzzzz...
Обратим внимание, что между строками 6 и 8 мы явно не уничтожали объект call и не освобождали линию. Это произошло автоматически при выталкивании объекта call из стека и вызове его деструктора. Однако, если создавать объект call не на стеке, а в куче с помощью оператора new, данный пример работать не будет, так как в таком случае мы должны явно удалить объект с помощью вызова delete.
Отметим основные моменты:
Книги по теме
Идиома очень простая и кратко описывается следующим образом: в конструкторе объект получает доступ к какому либо ресурсу (например, открывается файл или устанавливается соединение по сети к базе данных) и сохраняет описатель ресурса в закрытый члена класса, а при вызове деструктура этот ресурс освобождается (закрывается файл или соединение к БД). При объявлении объекта данного класса на стеке происходит и его инициализация с вызовом конструктора, захватывающий ресурс. При выходе из области видимости объект выталкивается из стека, но перед этим вызывается деструктор объекта, который и освобождает захваченный ресурс.
Например, если мы хотим поговорить с кем-то по телефону, то при инициализации разговора мы снимаем трубку, при этом захватываем ресурс под названием “Телефонная Линия”. Вдоволь наговорившись с собеседником, мы прерываем разговор и кладем трубку и одновременно освобождаем телефонную линию. Попробуем написать это на C++. Сначала определим класс, который будет символизировать ресурс, который мы захватываем. Класс не имеет состояния и содержит всего две функции-члена: pickThePhoneUp() – поднимает трубку и занимает телефонную линию, и putThePhoneDown() – кладет трубку и освобождает линию:
class TelephoneLine
{
public:
void pickUpThePhoneUp()
{
std::cout << "Line locked\n";
}
void putThePhoneDown()
{
std::cout << "Line unlocked\n";
}
};
Далее напишем класс, который будет описывать телефонный разговор. Класс содержит один закрытый член, который является экземпляром класса TelephoneLine и описывает состояние телефонной линии. Это и есть ресурс, который мы захватываем при создании, и освобождаем при уничтожении объекта:
class TelephoneCall
{
public:
TelephoneCall()
{
telephoneLine = new TelephoneLine();
telephoneLine->pickUpThePhoneUp();
}
~TelephoneCall()
{
telephoneLine->putThePhoneDown();
delete telephoneLine;
}
private:
TelephoneCall (const TelephoneCall &);
TelephoneCall& operator=(const TelephoneCall &);
TelephoneLine * telephoneLine;
};
Обработка возможных исключений при создании объекта класса TelephoneLine здесь намеренно опущена для того, чтобы не загромождать пример. Копирующий конструктор и оператор копирующего присваивания (строки 15 и 16) намеренно сделаны закрытыми и оставлены без реализации для того, чтобы предотвратить копирование объекта, так как в подобном случае получится что уже два и более объекта обладают одним ресурсом, а это может привести к непредсказуемым последствиям.
Теперь воспользуемся написанными классами:
int main()
{
{
std::cout << "Let's make a call to a friend.\n";
TelephoneCall call;
std::cout << "Oh, we've talked enough. I need to take a nap. Goodbye!\n";
}
std::cout << "Zzzzzz...";
}
Фигурные скобки в строке 3 и 7 используются для ограничения области видимости объекта, описывающего телефонный разговор — call. При создании объекта call происходит поднятие телефонной трубки и захват линии. При выходе из области видимости, ограниченной фигурными скобками, объект call уничтожается, при этом автоматически происходит вызов функции-члена putThePhoneDown() из деструктора, и, таким образом, ресурс “Телефонная линия” освобождается.
После запуска мы увидим следующий вывод:
Let's make a call to a friend.
Line locked
Oh, we've talked enough. I need to take a nap. Goodbye!
Line unlocked
Zzzzzz...
Обратим внимание, что между строками 6 и 8 мы явно не уничтожали объект call и не освобождали линию. Это произошло автоматически при выталкивании объекта call из стека и вызове его деструктора. Однако, если создавать объект call не на стеке, а в куче с помощью оператора new, данный пример работать не будет, так как в таком случае мы должны явно удалить объект с помощью вызова delete.
Отметим основные моменты:
- Обращение к ресурсу происходит в один этап. Либо мы получаем готовый полностью функциональный объект сразу, либо не получаем ничего.
- Безопасность по отношению к исключению. Например, если после создания объекта и обращения к ресурсу произойдет исключение и мы перейдем к обработчику исключения, мы можем быть уверены что ресурс освободится без нашего участия. Даже если ресурсов несколько, мы уверены что все они будут корректно освобождены. В противном случае, если захватывать и освобождать ресурс вручную, то при возникновении исключения нужно учитывать, какие ресурсы уже захвачены, а какие – нет, и освобождать только использованные ресурсы, что не слишком просто.
- Идиома очень удобна, когда нужно отслеживать важные ресурсы, а при этом сопровождение кода оставляет желать лучшего.
- Часто при использовании нескольких ресурсов освобождать их следует в обратном порядке. При использовании идиомы RAII, вследствие того что объекты с захваченными ресурсами располагаются на стеке, их уничтожение происходит в обратном порядке, что как правило и является желательным.
- Поддержка принципа DRY (Don’t Repeat Yourself). Код инициализации и освобождения ресурса содержится только в одном месте. Нет необходимости копировать и вставлять код инициализации в каждое место в программе где это необходимо. Достаточно просто создать объект.
- При необходимости использовать дополнительные параметры для обращения к ресурсу (например, логин и пароль к БД) эти параметры могут быть переданы в качестве аргументов конструктора.
- Накладные расходы при простейшей реализации обращения к ресурсу минимальны. В С++, как правило, при оптимизации компилятор реализует невиртуальные конструкторы и деструкторы в виде inline-функций.
- Данная идиома применима только в языках с предсказуемым временем жизни объекта. Сюда относится, например, С++, а также языки с сборщиком мусора, где время жизни объекта определяется количеством ссылок на него, такие как Objective C.
- Эта идиома неприменима в таких языках как Java или С#, где невозможно предсказать когда объект будет удален.
Книги по теме
- Стивен К. Дьюхерст. “Скользкие места С++. Как избежать проблемы при проектировании и компиляции ваших программ.” (С++ Gotchas. Avoiding Common Problems in Coding and Design). “Совет 67. Помните, что захват ресурса есть инициализация”.
- Стивен К. Дьюхерст. “C++. Священные знания” (“C++. Common Knowledge”). “Тема 40. Методика RAII”.
- Герб Саттер, Андрей Александреску. ”Стандарты программирования на С++ “. “Глава 13. Ресурсы должны быть во владении объектов”.
0 комментариев