Pull to refresh

Идиома 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. Ресурсы должны быть во владении объектов”.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.