Как стать автором
Обновить

Идиома RAII — захват ресурса есть инициализация

RAII – идиома, получившая широкое распространение благодаря создателю С++ Бьярну Страуструпу, и расшифровывается как “Resource Acquisition is Initialization” – захват ресурса есть инициализация.

Идиома очень простая и кратко описывается следующим образом: в конструкторе объект получает доступ к какому либо ресурсу (например, открывается файл или устанавливается соединение по сети к базе данных) и сохраняет описатель ресурса в закрытый члена класса, а при вызове деструктура этот ресурс освобождается (закрывается файл или соединение к БД). При объявлении объекта данного класса на стеке происходит и его инициализация с вызовом конструктора, захватывающий ресурс. При выходе из области видимости объект выталкивается из стека, но перед этим вызывается деструктор объекта, который и освобождает захваченный ресурс.

Например, если мы хотим поговорить с кем-то по телефону, то при инициализации разговора мы снимаем трубку, при этом захватываем ресурс под названием “Телефонная Линия”. Вдоволь наговорившись с собеседником, мы прерываем разговор и кладем трубку и одновременно освобождаем телефонную линию. Попробуем написать это на C++. Сначала определим класс, который будет символизировать ресурс, который мы захватываем. Класс не имеет состояния и содержит всего две функции-члена: pickThePhoneUp() – поднимает трубку и занимает телефонную линию, и putThePhoneDown() – кладет трубку и освобождает линию:
  1. class TelephoneLine
  2. {
  3. public:
  4.   void pickUpThePhoneUp()
  5.   { 
  6.      std::cout << "Line locked\n";
  7.   }
  8.   void putThePhoneDown()
  9.   {
  10.      std::cout << "Line unlocked\n";
  11.   }
  12. };
* This source code was highlighted with Source Code Highlighter.
Далее напишем класс, который будет описывать телефонный разговор. Класс содержит один закрытый член, который является экземпляром класса TelephoneLine и описывает состояние телефонной линии. Это и есть ресурс, который мы захватываем при создании, и освобождаем при уничтожении объекта:
  1. class TelephoneCall
  2. {
  3. public:
  4.   TelephoneCall()
  5.   {
  6.      telephoneLine = new TelephoneLine();
  7.      telephoneLine->pickUpThePhoneUp(); 
  8.   }
  9.   ~TelephoneCall()
  10.   {
  11.      telephoneLine->putThePhoneDown();
  12.      delete telephoneLine;
  13.   }
  14. private:
  15.   TelephoneCall (const TelephoneCall &);
  16.   TelephoneCall& operator=(const TelephoneCall &);
  17.   TelephoneLine * telephoneLine;
  18. };
* This source code was highlighted with Source Code Highlighter.
Обработка возможных исключений при создании объекта класса TelephoneLine здесь намеренно опущена для того, чтобы не загромождать пример. Копирующий конструктор и оператор копирующего присваивания (строки 15 и 16) намеренно сделаны закрытыми и оставлены без реализации для того, чтобы предотвратить копирование объекта, так как в подобном случае получится что уже два и более объекта обладают одним ресурсом, а это может привести к непредсказуемым последствиям.

Теперь воспользуемся написанными классами:
  1. int main()
  2. {
  3.   {
  4.      std::cout << "Let's make a call to a friend.\n";
  5.      TelephoneCall call;
  6.      std::cout << "Oh, we've talked enough. I need to take a nap. Goodbye!\n";
  7.   }
  8.   std::cout << "Zzzzzz...";
  9. }
* This source code was highlighted with Source Code Highlighter.
Фигурные скобки в строке 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.

Отметим основные моменты:

  1. Обращение к ресурсу происходит в один этап. Либо мы получаем готовый полностью функциональный объект сразу, либо не получаем ничего.
  2. Безопасность по отношению к исключению. Например, если после создания объекта и обращения к ресурсу произойдет исключение и мы перейдем к обработчику исключения, мы можем быть уверены что ресурс освободится без нашего участия. Даже если ресурсов несколько, мы уверены что все они будут корректно освобождены. В противном случае, если захватывать и освобождать ресурс вручную, то при возникновении исключения нужно учитывать, какие ресурсы уже захвачены, а какие – нет, и освобождать только использованные ресурсы, что не слишком просто.
  3. Идиома очень удобна, когда нужно отслеживать важные ресурсы, а при этом сопровождение кода оставляет желать лучшего.
  4. Часто при использовании нескольких ресурсов освобождать их следует в обратном порядке. При использовании идиомы RAII, вследствие того что объекты с захваченными ресурсами располагаются на стеке, их уничтожение происходит в обратном порядке, что как правило и является желательным.
  5. Поддержка принципа DRY (Don’t Repeat Yourself). Код инициализации и освобождения ресурса содержится только в одном месте. Нет необходимости копировать и вставлять код инициализации в каждое место в программе где это необходимо. Достаточно просто создать объект.
  6. При необходимости использовать дополнительные параметры для обращения к ресурсу (например, логин и пароль к БД) эти параметры могут быть переданы в качестве аргументов конструктора.
  7. Накладные расходы при простейшей реализации обращения к ресурсу минимальны. В С++, как правило, при оптимизации компилятор реализует невиртуальные конструкторы и деструкторы в виде inline-функций.
  8. Данная идиома применима только в языках с предсказуемым временем жизни объекта. Сюда относится, например, С++, а также языки с сборщиком мусора, где время жизни объекта определяется количеством ссылок на него, такие как Objective C.
  9. Эта идиома неприменима в таких языках как Java или С#, где невозможно предсказать когда объект будет удален.


Книги по теме
  1. Стивен К. Дьюхерст. “Скользкие места С++. Как избежать проблемы при проектировании и компиляции ваших программ.” (С++ Gotchas. Avoiding Common Problems in Coding and Design). “Совет 67. Помните, что захват ресурса есть инициализация”.
  2. Стивен К. Дьюхерст. “C++. Священные знания” (“C++. Common Knowledge”). “Тема 40. Методика RAII”.
  3. Герб Саттер, Андрей Александреску. ”Стандарты программирования на С++ “. “Глава 13. Ресурсы должны быть во владении объектов”.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.
Изменить настройки темы