пятница, 23 июля 2010 г.

Идиома RAII

Resource Acquisition Is Initialization – «Завладение ресурсом есть его инициализация». Если вдуматься в смысл, то чем-то напоминает ленивую инициализацию.
Однако для нас, для C++ программистов, наиболее важным является следствие из этой идиомы: «Освобождение ресурса есть его деинициализация».

К примеру, нужно скопировать содержимое одного файла в другой:

FILE* file1 = fopen("first.bin","rb");
if (!file1)
throw;

FILE* file2 = fopen("second.bin","wb");
if (!file2)
{
fclose(file1);
throw;
}

fseek(file1,0,SEEK_END);
unsigned long size = ftell(file1);
rewind(file1);

char* buffer = (char*)malloc(sizeof(char)*size);
if (!buffer)
{
fclose(file2);
fclose(file1);
throw;
}

unsigned long result = fread (buffer,1,size,file1);
if (result != size)
{
free(buffer);
fclose(file2);
fclose(file1);
throw;
}

result = fwrite(buffer,1,size,file2);
if (result != size)
{
free(buffer);
fclose(file2);
fclose(file1);
throw;
}

free(buffer);
fclose(file2);
fclose(file1);

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

Давайте подумаем над недостатками такого кода. Вносить изменения в него крайне опасно: что-нибудь пропустишь – и ресурс (память или файл) останется неосвобожденным. Кроме того, если по каким-либо причинам исключение произойдет при работе с файлом через fread или fwrite, то все ресурсы останутся жить в зомбиленде. Кроме того, присутствует дублирование кода, читабельность стремиться к нулю со всеми вытекающими.

И тут приходят на помощь особенности языка C++. Давайте вспомним локальные переменные и области видимости.

{
int i;
MyClass myClass;

...
} // выход из области видимости

Что произойдет с переменными i и myClass по достижению конца области видимости? Переменная i будет вытолкнута из стека, а для myClass будет вызван деструктор класса. А ведь это уже интересно...

{
int i;
MyClass myClass;

...
if (i==1)
return 0;

if (i==2)
throw;
}

Когда в этом случае будет вызван деструктор myClass? Правильно, в каждом из случаев выхода из области видимости: если i=1, то перед return, если i=2 – перед генерацией исключения. При всех остальных значениях i – естественным путем, так как достигнута закрывающая скобка.
Это напоминает первый пример. Если написать откаты в деструкторах объектов, то не нужно их специально вызывать: они будут вызваны автоматически, когда это понадобится. И не останется зомби-файлов и утечек памяти, даже случайно. Ну, и по аналогии, в конструкторе нужно инициализировать объекты. Это и есть RAII.
Пишем код, лишенный всех недостатков. Сначала определяем классы, которые будут служить RAII-врапперами:

class File // RAII обертка для файла
{
public:
File(char* name, char* mode)
{
file_ = fopen(name,mode);
if (!file_)
throw;
}

~File()
{
if (file_)
fclose(file_);
}

void fread(char* buffer, unsigned long elements, unsigned long bytes)
{
if (::fread(buffer,elements,bytes,file_) != bytes)
throw;
}

void fwrite(char* buffer, unsigned long elements, unsigned long bytes)
{
if (::fwrite(buffer,elements,bytes,file_) != bytes)
throw;
}

unsigned long GetSize()
{
fseek(file_,0,SEEK_END);
unsigned long size = ftell(file_);
rewind(file_);

return size;
}

private:
FILE* file_;
};

class Memory // RAII обертка для памяти
{
public:
Memory(unsigned long size)
{
memory_ = (char*) malloc (size);

if (!memory_)
throw;
}

~Memory()
{
if (memory_)
free(memory_);
}

char* Get()
{
return memory_;
}
};

И, собственно, рефакторинг участка кода:

{
File file1("file1.bin","rb");
File file2("file2.bin","wb");

unsigned long fileSize = file1.GetSize();

Memory buffer(fileSize);

file1.fread(buffer.Get(),1,fileSize);
file2.fwrite(buffer.Get(),1,fileSize);
}

Вот и все! Память больше освобождать не нужно: она освободится автоматически, когда переменная buffer станет недоступна. И с закрытиями файлов также: все будет закрыто при выходе из области видимости переменных file1 и file2.
Стоит ли использовать идиому RAII? Поверьте, стоит. Даже если у вас феноменальная память и bounds checker в голове, рано или поздно вы допустите ошибку. Так почему бы не использовать RAII, хуже то от этого не становится.

P.S. Обмозговав класс Memory, можно прийти к выводу, что механизм автоматического освобождения памяти через RAII очень даже хорош. Но не спешите писать свой класс Memory: он уже есть в библиотеке STL, правильное его название «интеллектуальный указатель», а класс называется std::auto_ptr.

1 комментарий:

  1. Спасибо, человечище!! Так сложно отыскать нормальную, перевариваемую инфу среди этого мусора в сети!

    ОтветитьУдалить