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

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

Идиома очень простая и кратко описывается следующим образом: в конструкторе объект получает доступ к какому либо ресурсу (например, открывается файл или устанавливается соединение по сети к базе данных) и сохраняет описатель ресурса в закрытый члена класса, а при вызове деструктура этот ресурс освобождается (закрывается файл или соединение к БД). При объявлении объекта данного класса на стеке происходит и его инициализация с вызовом конструктора, захватывающий ресурс. При выходе из области видимости объект выталкивается из стека, но перед этим вызывается деструктор объекта, который и освобождает захваченный ресурс.
Например, если мы хотим поговорить с кем-то по телефону, то при инициализации разговора мы снимаем трубку, при этом захватываем ресурс под названием “Телефонная Линия”. Вдоволь наговорившись с собеседником, мы прерываем разговор и кладем трубку и одновременно освобождаем телефонную линию. Попробуем написать это на 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.

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

  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. Ресурсы должны быть во владении объектов”.


0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.