Язык программирования C++ от Страуструпа

Запросы ресурсов


Если в некоторой функции потребуются определенные ресурсы, например, нужно открыть файл, отвести блок памяти в области свободной памяти, установить монопольные права доступа и т.д., для дальнейшей работы системы обычно бывает крайне важно, чтобы ресурсы были освобождены надлежащим образом. Обычно такой "надлежащий способ" реализует функция, в которой происходит запрос ресурсов и освобождение их перед выходом. Например:

void use_file(const char* fn)

{

  FILE* f = fopen(fn,"w");         // работаем с f

  fclose(f);

}

Все это выглядит вполне нормально до тех пор, пока вы не поймете, что при любой ошибке, происшедшей после вызова fopen() и до вызова fclose(), возникнет особая ситуация, в результате которой мы выйдем из use_file(), не обращаясь к fclose(). Стоит сказать, что та же проблема возникает и в языках, не поддерживающих особые ситуации. Так, обращение к функции longjump()из стандартной библиотеки С может иметь такие же неприятные последствия.

Если вы создаете устойчивую к ошибкам системам, эту проблему придется решать. Можно дать примитивное решение:

void use_file(const char* fn)

{

  FILE* f = fopen(fn,"w");

  try {

     // работаем с f



  }

  catch (...) {

     fclose(f);

     throw;

  }

  fclose(f);

}

Вся часть функции, работающая с файлом f, помещена в проверяемый блок, в котором перехватываются все особые ситуации, закрывается файл и особая ситуация запускается повторно.

Недостаток этого решения в его многословности, громоздкости и потенциальной расточительности. К тому же всякое многословное и громоздкое решение чревато ошибками, хотя бы в силу усталости программиста. К счастью, есть более приемлемое решение. В общем виде проблему можно сформулировать так:

void acquire()

{

  // запрос ресурса 1

  // ...

  // запрос ресурса n

  // использование ресурсов

  // освобождение ресурса n

  // ...

  // освобождение ресурса 1

}

Как правило бывает важно, чтобы ресурсы освобождались в обратном по сравнению с запросами порядке. Это очень сильно напоминает порядок работы с локальными объектами, создаваемыми конструкторами и уничтожаемыми деструкторами. Поэтому мы можем решить проблему запроса и освобождения ресурсов, если будем использовать подходящие объекты классов с конструкторами и деструкторами. Например, можно определить класс FilePtr, который выступает как тип FILE* :

class FilePtr {

  FILE* p;

  public:

     FilePtr(const char* n, const char* a)

       { p = fopen(n,a); }

     FilePtr(FILE* pp) { p = pp; }

       ~FilePtr() { fclose(p); }

     operator FILE*() { return p; }

};

Построить объект FilePtr можно либо, имея объект типа FILE*, либо, получив нужные для fopen() параметры. В любом случае этот объект будет уничтожен при выходе из его области видимости, и его деструктор закроет файл. Теперь наш пример сжимается до такой функции:

void use_file(const char* fn)

{

  FilePtr f(fn,"w");

  // работаем с f

}

Деструктор будет вызываться независимо от того, закончилась ли функция нормально, или произошел запуск особой ситуации.



Содержание раздела